203 lines
9.4 KiB
TypeScript
203 lines
9.4 KiB
TypeScript
|
|
|
|||
|
|
import React, { useState, useEffect } from 'react';
|
|||
|
|
import { District, Building, DomaApplication, Employee } from '../../types';
|
|||
|
|
import { PerformanceCard } from './PerformanceCard';
|
|||
|
|
import { EditDistrictModal } from './EditDistrictModal';
|
|||
|
|
import { backendApi } from '../../services/apiClient';
|
|||
|
|
import { Plus } from 'lucide-react';
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
aggregatedData: any;
|
|||
|
|
onSelectDistrict: (d: District) => void;
|
|||
|
|
canManage: boolean;
|
|||
|
|
onAddDistrict: (payload: { name: string; managerName: string }) => void;
|
|||
|
|
onDeleteDistrict: (d: District) => void;
|
|||
|
|
onViewStaff: (d: District) => void;
|
|||
|
|
onDistrictUpdated?: () => void;
|
|||
|
|
role: string;
|
|||
|
|
employees?: Employee[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const DistrictsSummary: React.FC<Props> = ({
|
|||
|
|
aggregatedData,
|
|||
|
|
onSelectDistrict,
|
|||
|
|
canManage,
|
|||
|
|
onAddDistrict,
|
|||
|
|
onDeleteDistrict,
|
|||
|
|
onViewStaff,
|
|||
|
|
onDistrictUpdated,
|
|||
|
|
role,
|
|||
|
|
employees = [],
|
|||
|
|
}) => {
|
|||
|
|
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
|||
|
|
const [editDistrict, setEditDistrict] = useState<District | null>(null);
|
|||
|
|
const [name, setName] = useState('');
|
|||
|
|
const [managerName, setManagerName] = useState('Не назначен');
|
|||
|
|
const [availableEmployees, setAvailableEmployees] = useState<Employee[]>([]);
|
|||
|
|
const [loadingEmployees, setLoadingEmployees] = useState(false);
|
|||
|
|
|
|||
|
|
// Загружаем список сотрудников при открытии формы
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (isCreateOpen) {
|
|||
|
|
fetchEmployees();
|
|||
|
|
}
|
|||
|
|
}, [isCreateOpen]);
|
|||
|
|
|
|||
|
|
const fetchEmployees = async () => {
|
|||
|
|
try {
|
|||
|
|
setLoadingEmployees(true);
|
|||
|
|
const data = await backendApi.getEmployees();
|
|||
|
|
setAvailableEmployees(Array.isArray(data) ? data : []);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error fetching employees:', error);
|
|||
|
|
setAvailableEmployees([]);
|
|||
|
|
} finally {
|
|||
|
|
setLoadingEmployees(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const UNASSIGNED_MANAGER = 'Не назначен';
|
|||
|
|
|
|||
|
|
// Фильтруем сотрудников: только мастера и начальники участка
|
|||
|
|
const eligibleManagers = availableEmployees.filter(emp => {
|
|||
|
|
if (emp.status !== 'active') return false;
|
|||
|
|
const positionLower = emp.position.toLowerCase();
|
|||
|
|
return positionLower.includes('мастер') || positionLower.includes('начальник участка');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (!name.trim()) return;
|
|||
|
|
onAddDistrict({ name: name.trim(), managerName: managerName === '' ? UNASSIGNED_MANAGER : managerName.trim() });
|
|||
|
|
setIsCreateOpen(false);
|
|||
|
|
setName('');
|
|||
|
|
setManagerName(UNASSIGNED_MANAGER);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-6 animate-fade-in">
|
|||
|
|
<div className="flex justify-between items-end px-1">
|
|||
|
|
<div className="flex flex-col">
|
|||
|
|
<h2 className="text-xl font-bold text-slate-800">Обзор участков</h2>
|
|||
|
|
<span className="text-[10px] text-slate-500 font-medium uppercase mt-1">
|
|||
|
|
{role === 'MASTER' ? 'Моя зона ответственности' : 'Контроль по всей компании'}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
{canManage && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => setIsCreateOpen(true)}
|
|||
|
|
className="bg-primary-600 text-white p-2 rounded-xl shadow-lg flex items-center gap-2 px-3 text-xs font-bold active:scale-95 transition-transform"
|
|||
|
|
>
|
|||
|
|
<Plus className="w-4 h-4" /> Участок
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{isCreateOpen && canManage && (
|
|||
|
|
<form
|
|||
|
|
onSubmit={handleSubmit}
|
|||
|
|
className="bg-white border border-slate-200 rounded-2xl p-4 flex flex-col md:flex-row gap-3 items-stretch md:items-end shadow-sm"
|
|||
|
|
>
|
|||
|
|
<div className="flex-1">
|
|||
|
|
<label className="block text-[10px] font-black uppercase text-slate-400 mb-1">
|
|||
|
|
Название участка
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
value={name}
|
|||
|
|
onChange={(e) => setName(e.target.value)}
|
|||
|
|
placeholder="Например: Участок №3 (Южный)"
|
|||
|
|
className="w-full border border-slate-200 rounded-xl px-3 py-2 text-sm focus:ring-2 focus:ring-primary-500 outline-none"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex-1">
|
|||
|
|
<label className="block text-[10px] font-black uppercase text-slate-400 mb-1">
|
|||
|
|
Начальник участка
|
|||
|
|
</label>
|
|||
|
|
{loadingEmployees ? (
|
|||
|
|
<div className="w-full border border-slate-200 rounded-xl px-3 py-2 text-sm text-slate-400 bg-slate-50">
|
|||
|
|
Загрузка сотрудников...
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<select
|
|||
|
|
value={managerName}
|
|||
|
|
onChange={(e) => setManagerName(e.target.value)}
|
|||
|
|
className="w-full border border-slate-200 rounded-xl px-3 py-2 text-sm focus:ring-2 focus:ring-primary-500 outline-none bg-white"
|
|||
|
|
>
|
|||
|
|
<option value={UNASSIGNED_MANAGER}>Не назначен</option>
|
|||
|
|
{eligibleManagers.length === 0 ? (
|
|||
|
|
<option value="" disabled>Нет доступных мастеров или начальников участка</option>
|
|||
|
|
) : (
|
|||
|
|
eligibleManagers.map(emp => (
|
|||
|
|
<option key={emp.id} value={emp.name}>
|
|||
|
|
{emp.name} - {emp.position}
|
|||
|
|
</option>
|
|||
|
|
))
|
|||
|
|
)}
|
|||
|
|
</select>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => {
|
|||
|
|
setIsCreateOpen(false);
|
|||
|
|
setName('');
|
|||
|
|
setManagerName('Не назначен');
|
|||
|
|
}}
|
|||
|
|
className="px-4 py-2 rounded-xl border border-slate-200 text-xs font-bold text-slate-500 bg-white hover:bg-slate-50"
|
|||
|
|
>
|
|||
|
|
Отмена
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="submit"
|
|||
|
|
className="px-4 py-2 rounded-xl bg-primary-600 text-white text-xs font-bold hover:bg-primary-700 active:scale-95 transition-transform"
|
|||
|
|
>
|
|||
|
|
Сохранить
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-1 gap-4">
|
|||
|
|
{Object.values(aggregatedData).map((data: any) => {
|
|||
|
|
const assignedToDistrict = employees.filter(emp => {
|
|||
|
|
const ids = emp.assignedDistrictIds?.length ? emp.assignedDistrictIds : (emp.assignedDistrictId ? [emp.assignedDistrictId] : []);
|
|||
|
|
return ids.includes(data.district.id);
|
|||
|
|
});
|
|||
|
|
const staffCount = assignedToDistrict.length;
|
|||
|
|
const managerFromDb = data.district.managerName?.trim();
|
|||
|
|
const managerDisplay = (managerFromDb && managerFromDb !== UNASSIGNED_MANAGER)
|
|||
|
|
? managerFromDb
|
|||
|
|
: (assignedToDistrict.length > 0 ? assignedToDistrict[0].name : UNASSIGNED_MANAGER);
|
|||
|
|
return (
|
|||
|
|
<PerformanceCard
|
|||
|
|
key={data.district.id}
|
|||
|
|
title={data.district.name}
|
|||
|
|
subtitle={managerDisplay}
|
|||
|
|
applications={data.applications}
|
|||
|
|
onClick={() => onSelectDistrict(data.district)}
|
|||
|
|
type="district"
|
|||
|
|
buildingCount={data.buildings.length}
|
|||
|
|
staffCount={staffCount}
|
|||
|
|
onDelete={canManage ? () => onDeleteDistrict(data.district) : undefined}
|
|||
|
|
onViewStaff={() => onViewStaff(data.district)}
|
|||
|
|
onEdit={canManage ? () => setEditDistrict(data.district) : undefined}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{editDistrict && (
|
|||
|
|
<EditDistrictModal
|
|||
|
|
district={editDistrict}
|
|||
|
|
onClose={() => setEditDistrict(null)}
|
|||
|
|
onSaved={() => {
|
|||
|
|
onDistrictUpdated?.();
|
|||
|
|
}}
|
|||
|
|
employees={employees}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|