import React, { useState, useEffect, useCallback } from 'react'; import { INITIAL_EMPLOYEES, EmployeeEntity } from '../../mock/mockData'; import { getStoreItem, putStoreItem } from '../../services/storage/indexedDb'; import { Badge, Button, MaturityBadge, Modal, useToast } from '../../shared/ui'; import { Clock, Star, Phone, Plus, Calendar, Search, AlertTriangle, } from 'lucide-react'; interface ShiftPlanItem { employeeId: string; day: string; // 'Пн' | 'Вт' | 'Ср' | 'Чт' | 'Пт' | 'Сб' | 'Вс' shiftText: string; hours: number; hasConflict?: boolean; conflictReason?: string; } const DAYS_OF_WEEK = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; const INITIAL_SHIFTS: ShiftPlanItem[] = [ { employeeId: 'emp-1', day: 'Пн', shiftText: '10:00 - 22:00', hours: 12 }, { employeeId: 'emp-1', day: 'Вт', shiftText: '10:00 - 22:00', hours: 12 }, { employeeId: 'emp-1', day: 'Ср', shiftText: '10:00 - 22:00', hours: 12 }, { employeeId: 'emp-1', day: 'Чт', shiftText: '10:00 - 22:00', hours: 12 }, { employeeId: 'emp-1', day: 'Пт', shiftText: '09:00 - 23:00', hours: 14, hasConflict: true, conflictReason: 'Смена превышает 12 ч (ТК РФ)' }, { employeeId: 'emp-2', day: 'Вт', shiftText: '11:00 - 23:00', hours: 12 }, { employeeId: 'emp-2', day: 'Ср', shiftText: '11:00 - 23:00', hours: 12 }, { employeeId: 'emp-2', day: 'Чт', shiftText: '11:00 - 23:00', hours: 12 }, { employeeId: 'emp-2', day: 'Сб', shiftText: '11:00 - 23:00', hours: 12 }, { employeeId: 'emp-2', day: 'Вс', shiftText: '11:00 - 23:00', hours: 12 }, { employeeId: 'emp-3', day: 'Пн', shiftText: '10:00 - 22:00', hours: 12 }, { employeeId: 'emp-3', day: 'Чт', shiftText: '10:00 - 22:00', hours: 12 }, { employeeId: 'emp-3', day: 'Пт', shiftText: '10:00 - 22:00', hours: 12 }, { employeeId: 'emp-3', day: 'Сб', shiftText: '10:00 - 22:00', hours: 12 }, { employeeId: 'emp-4', day: 'Пн', shiftText: '12:00 - 00:00', hours: 12 }, { employeeId: 'emp-4', day: 'Вт', shiftText: '12:00 - 00:00', hours: 12 }, { employeeId: 'emp-4', day: 'Пт', shiftText: '12:00 - 00:00', hours: 12 }, { employeeId: 'emp-4', day: 'Сб', shiftText: '12:00 - 00:00', hours: 12 }, ]; export const EmployeesModule: React.FC = () => { const { showToast } = useToast(); const [activeTab, setActiveTab] = useState<'employees' | 'schedule'>('employees'); const [employees, setEmployees] = useState(INITIAL_EMPLOYEES); const [shifts, setShifts] = useState(INITIAL_SHIFTS); const [search, setSearch] = useState(''); // Load persisted employees and shifts on mount useEffect(() => { async function loadData() { try { const savedEmp = await getStoreItem<{ key: string; employees: EmployeeEntity[] }>('settings', 'krona_persisted_employees'); if (savedEmp?.employees && Array.isArray(savedEmp.employees) && savedEmp.employees.length > 0) { setEmployees(savedEmp.employees); } const savedShifts = await getStoreItem<{ key: string; shifts: ShiftPlanItem[] }>('settings', 'krona_persisted_shifts'); if (savedShifts?.shifts && Array.isArray(savedShifts.shifts) && savedShifts.shifts.length > 0) { setShifts(savedShifts.shifts); } } catch { // Keep initial } } loadData(); }, []); const persistEmployees = useCallback((updated: EmployeeEntity[]) => { setEmployees(updated); void putStoreItem('settings', { key: 'krona_persisted_employees', employees: updated }).catch(() => {}); }, []); const persistShifts = useCallback((updated: ShiftPlanItem[]) => { setShifts(updated); void putStoreItem('settings', { key: 'krona_persisted_shifts', shifts: updated }).catch(() => {}); }, []); // New Employee Modal const [isNewEmpModalOpen, setIsNewEmpModalOpen] = useState(false); const [newEmpName, setNewEmpName] = useState(''); const [newEmpRole, setNewEmpRole] = useState('Официант'); const [newEmpPhone, setNewEmpPhone] = useState('+7 (913) '); const [newEmpRate, setNewEmpRate] = useState(250); // Edit Shift Modal const [editingShift, setEditingShift] = useState<{ empId: string; day: string; shiftText: string; hours: number } | null>(null); const filteredEmployees = employees.filter((e) => { const matchSearch = e.name.toLowerCase().includes(search.toLowerCase()) || e.role.toLowerCase().includes(search.toLowerCase()); return matchSearch; }); const handleCreateEmployee = (e: React.FormEvent) => { e.preventDefault(); if (!newEmpName.trim()) return; const initials = newEmpName.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase() || 'НС'; const newEmp: EmployeeEntity = { id: `emp-${Date.now()}`, name: newEmpName.trim(), role: newEmpRole, status: 'off_shift', shiftHours: '09:00 - 21:00', salesToday: 0, rating: 5.0, phone: newEmpPhone, avatarInitials: initials, }; const updatedEmployees = [newEmp, ...employees]; persistEmployees(updatedEmployees); setIsNewEmpModalOpen(false); setNewEmpName(''); showToast({ title: 'Сотрудник добавлен', message: `Карточка «${newEmp.name}» (${newEmp.role}) успешно создана`, type: 'success', }); }; const handleSaveShift = () => { if (!editingShift) return; const hasConflict = editingShift.hours > 12; const conflictReason = hasConflict ? 'Смена превышает 12 ч (ТК РФ)' : undefined; const filtered = shifts.filter( (s) => !(s.employeeId === editingShift.empId && s.day === editingShift.day) ); const updatedShifts = (editingShift.shiftText === 'Выходной' || editingShift.hours === 0) ? filtered : [ ...filtered, { employeeId: editingShift.empId, day: editingShift.day, shiftText: editingShift.shiftText, hours: editingShift.hours, hasConflict, conflictReason, }, ]; persistShifts(updatedShifts); setEditingShift(null); showToast({ title: 'Смена обновлена', message: `График на ${editingShift.day} сохранен`, type: 'success', }); }; const conflictCount = shifts.filter((s) => s.hasConflict).length; return (
{/* Header */}

Сотрудники, графики и табель учета смен

Управление персоналом, планировщик смен, контроль переработок и предупреждения о конфликтах расписания

{/* 1. EMPLOYEES TAB */} {activeTab === 'employees' && (
setSearch(e.target.value)} placeholder="Поиск по имени или должности..." className="w-full pl-8 pr-3 py-1.5 text-xs bg-krona-surface border border-krona-border rounded-lg text-krona-primary placeholder:text-krona-secondary focus:outline-none focus:border-krona-green" />
На смене: {employees.filter((e) => e.status === 'on_shift').length} Выходной: {employees.filter((e) => e.status !== 'on_shift').length}
{filteredEmployees.map((emp) => (
{emp.avatarInitials}

{emp.name}

{emp.role}
{emp.status === 'on_shift' ? 'На смене' : 'Выходной'}
График сегодня: {emp.shiftHours}
Рейтинг качества: {emp.rating} / 5.0
{emp.phone}
))}
)} {/* 2. SCHEDULE PLANNER TAB */} {activeTab === 'schedule' && (
Недельный график смен (07.09.2026 — 13.09.2026) {conflictCount > 0 ? ( {conflictCount} конфликт расписания! ) : ( Конфликтов нет )}

Кликните по ячейке для назначения или изменения времени смены сотрудника

{DAYS_OF_WEEK.map((d) => ( ))} {employees.map((emp) => { const empShifts = shifts.filter((s) => s.employeeId === emp.id); const totalHours = empShifts.reduce((acc, s) => acc + s.hours, 0); return ( {DAYS_OF_WEEK.map((day) => { const shift = empShifts.find((s) => s.day === day); return ( ); })} ); })}
Сотрудник / Должность {d} Часы / нед.
{emp.name}
{emp.role}
{ setEditingShift({ empId: emp.id, day, shiftText: shift ? shift.shiftText : '10:00 - 22:00', hours: shift ? shift.hours : 12, }); }} className="p-2 border-l border-krona-border/40 text-center cursor-pointer hover:bg-krona-workspace transition-colors" > {shift ? (
{shift.shiftText}
{shift.hours} ч
{shift.hasConflict && (
Превышение!
)}
) : ( — )}
40 ? 'text-amber-600' : 'text-krona-primary'}> {totalHours} ч
)} {/* Modal: New Employee */} {isNewEmpModalOpen && ( setIsNewEmpModalOpen(false)} title="Новый сотрудник" subtitle="Создание профиля и назначение должности" maxWidth="sm" footer={
} >
setNewEmpName(e.target.value)} placeholder="Иванова Екатерина Сергеевна" className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green" />
setNewEmpRate(Number(e.target.value))} className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green" />
setNewEmpPhone(e.target.value)} className="w-full p-2 bg-krona-workspace border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green" />
)} {/* Modal: Edit Shift */} {editingShift && ( setEditingShift(null)} title={`Назначение смены: ${editingShift.day}`} subtitle={`Сотрудник: ${employees.find((e) => e.id === editingShift.empId)?.name}`} maxWidth="sm" footer={
} >
{editingShift.hours > 12 && (
Внимание: смена длится {editingShift.hours} часов. Согласно Трудовому кодексу, стандартная смена не должна превышать 12 часов.
)}
)}
); };