import React, { useState, useEffect } from 'react'; import { X, Calendar, Clock, FileText, UserCheck, AlertCircle } from 'lucide-react'; import { Employee, EmployeeVacation, EmployeeSickLeave, EmployeeAbsence, User, UserRole } from '../../types'; import { authFetch } from '../../services/apiClient'; export type AbsenceType = 'vacation' | 'sick_leave' | 'day_off' | 'absence' | 'late' | 'early_leave'; interface WorkCalendarModalProps { employee: Employee; onClose: () => void; onSave: () => void; type?: AbsenceType; currentUser?: User; /** Список сотрудников для подстановки имени руководителя */ employees?: Employee[]; /** Сотрудники, от имени которых можно оформить запись (для смены имени в шапке) */ selectableEmployees?: Employee[]; /** Вызов при смене сотрудника в шапке модалки */ onEmployeeChange?: (employee: Employee) => void; } export const WorkCalendarModal: React.FC = ({ employee, onClose, onSave, type: initialType, currentUser, employees = [], selectableEmployees, onEmployeeChange, }) => { const [absenceType, setAbsenceType] = useState(initialType || 'vacation'); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [startTime, setStartTime] = useState(''); const [endTime, setEndTime] = useState(''); const [reason, setReason] = useState(''); const [notes, setNotes] = useState(''); const [requiresApproval, setRequiresApproval] = useState(true); const [approverSignature, setApproverSignature] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [vacationType, setVacationType] = useState<'annual' | 'unpaid' | 'study' | 'maternity' | 'other'>('annual'); const [sickLeaveNumber, setSickLeaveNumber] = useState(''); const [diagnosis, setDiagnosis] = useState(''); const [medicalInstitution, setMedicalInstitution] = useState(''); /** Предварительная дата выхода с больничного */ const [expectedReturnDate, setExpectedReturnDate] = useState(''); /** Флаг: пользователь вручную менял дату окончания отпуска (не перезаписывать +14) */ const [vacationEndManuallyChanged, setVacationEndManuallyChanged] = useState(false); const absenceTypeLabels: Record = { vacation: { label: 'Отпуск', icon: '🏖️', color: 'bg-blue-50 text-blue-600 border-blue-200' }, sick_leave: { label: 'Больничный', icon: '🏥', color: 'bg-red-50 text-red-600 border-red-200' }, day_off: { label: 'Отгул', icon: '📅', color: 'bg-amber-50 text-amber-600 border-amber-200' }, absence: { label: 'Прогул', icon: '⚠️', color: 'bg-orange-50 text-orange-600 border-orange-200' }, late: { label: 'Опоздание', icon: '⏰', color: 'bg-purple-50 text-purple-600 border-purple-200' }, early_leave: { label: 'Ранний уход', icon: '🚪', color: 'bg-indigo-50 text-indigo-600 border-indigo-200' }, }; const vacationTypeLabels: Record = { annual: 'Ежегодный', unpaid: 'Без сохранения зарплаты', study: 'Учебный', maternity: 'Декретный', other: 'Другой', }; const isSelf = currentUser && employee && currentUser.id === employee.id; /** Типы, которые сотрудник не может оформить себе: только руководитель или HR подчинённым */ const onlyManagerOrHrTypes: AbsenceType[] = ['absence', 'late', 'early_leave']; const allowedTypes = (Object.keys(absenceTypeLabels) as AbsenceType[]).filter( (t) => !isSelf || !onlyManagerOrHrTypes.includes(t) ); // Руководитель для согласования подтягивается автоматически; если руководителя нет — автовыбор HR useEffect(() => { if (!employee) return; if (employee.managerId && employees?.length) { const manager = employees.find((e) => e.id === employee.managerId); if (manager?.name) { setApproverSignature(manager.name); return; } } setApproverSignature('HR'); }, [employee?.id, employee?.managerId, employees]); // Отпуск: конец по умолчанию +14 дней от начала, с возможностью изменить useEffect(() => { if (absenceType !== 'vacation' || !startDate || vacationEndManuallyChanged) return; const start = new Date(startDate); start.setDate(start.getDate() + 14); setEndDate(start.toISOString().split('T')[0]); }, [absenceType, startDate, vacationEndManuallyChanged]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setLoading(true); try { // Валидация if (!startDate) { setError('Укажите дату начала'); setLoading(false); return; } if (absenceType === 'vacation' || absenceType === 'day_off' || absenceType === 'absence') { if (!endDate) { setError('Укажите дату окончания'); setLoading(false); return; } if (new Date(endDate) < new Date(startDate)) { setError('Дата окончания не может быть раньше даты начала'); setLoading(false); return; } } if (absenceType === 'sick_leave' && endDate && new Date(endDate) < new Date(startDate)) { setError('Дата окончания не может быть раньше даты начала'); setLoading(false); return; } if (absenceType === 'late' || absenceType === 'early_leave') { if (!startTime) { setError('Укажите время'); setLoading(false); return; } } const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; const baseUrl = (import.meta.env.DEV || !apiBaseUrl) ? '/api' : `${apiBaseUrl}`; let response: Response; if (absenceType === 'vacation') { // Создание отпуска const calculatedDays = Math.ceil((new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24)) + 1; response = await authFetch(`${baseUrl}/employees/${employee.id}/vacations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ startDate, endDate, daysCount: calculatedDays, vacationType, status: requiresApproval ? 'planned' : 'approved', requiresApproval, approvedBy: !requiresApproval ? 'Система' : undefined, approvedAt: !requiresApproval ? new Date().toISOString() : undefined, approvedSignature: !requiresApproval ? 'Автоматическое согласование' : approverSignature || undefined, notes, currentUserId: currentUser?.id, currentUserRole: currentUser?.role, }), }); } else if (absenceType === 'sick_leave') { // Создание больничного const calculatedDays = endDate ? Math.ceil((new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24)) + 1 : undefined; const sickLeaveUrl = `${baseUrl}/employees/${employee.id}/sick-leaves`; response = await authFetch(sickLeaveUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ startDate, endDate: endDate || null, expectedReturnDate: expectedReturnDate || null, daysCount: calculatedDays, sickLeaveNumber: sickLeaveNumber || null, diagnosis: diagnosis || null, medicalInstitution: medicalInstitution || null, status: 'active', requiresApproval, approvedBy: !requiresApproval ? 'Система' : undefined, approvedAt: !requiresApproval ? new Date().toISOString() : undefined, approvedSignature: !requiresApproval ? 'Автоматическое согласование' : approverSignature || undefined, notes, currentUserId: currentUser?.id, currentUserRole: currentUser?.role, }), }); } else { // Создание отгула/прогула/опоздания/раннего ухода let calculatedDays = 1.0; if (endDate) { calculatedDays = Math.ceil((new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24)) + 1; } else if (startTime && endTime) { // Для части дня рассчитываем долю дня const start = new Date(`2000-01-01T${startTime}`); const end = new Date(`2000-01-01T${endTime}`); const hours = (end.getTime() - start.getTime()) / (1000 * 60 * 60); calculatedDays = hours / 8; // Предполагаем 8-часовой рабочий день } response = await authFetch(`${baseUrl}/employees/${employee.id}/absences`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ absenceType: absenceType === 'day_off' ? 'day_off' : absenceType === 'absence' ? 'absence' : absenceType === 'late' ? 'late' : 'early_leave', startDate, endDate: endDate || null, startTime: startTime || null, endTime: endTime || null, daysCount: calculatedDays, reason: reason || null, requiresApproval, status: requiresApproval ? 'pending' : 'approved', approvedBy: !requiresApproval ? 'Система' : undefined, approvedAt: !requiresApproval ? new Date().toISOString() : undefined, approvedSignature: !requiresApproval ? 'Автоматическое согласование' : approverSignature || undefined, notes: notes || null, currentUserId: currentUser?.id, // ID текущего пользователя для проверки прав currentUserRole: currentUser?.role, // Роль текущего пользователя для проверки прав }), }); } if (response.ok) { onSave(); onClose(); } else { const errorData = await response.json(); setError(errorData.error || 'Ошибка при создании записи'); } } catch (err) { console.error('Error creating absence:', err); setError('Ошибка при создании записи'); } finally { setLoading(false); } }; const showDateRange = absenceType === 'vacation' || absenceType === 'sick_leave' || absenceType === 'day_off' || absenceType === 'absence'; const showTimeFields = absenceType === 'late' || absenceType === 'early_leave'; const showVacationFields = absenceType === 'vacation'; const showSickLeaveFields = absenceType === 'sick_leave'; return (
e.stopPropagation()} > {/* Header */}

Рабочий календарь

{selectableEmployees && selectableEmployees.length > 0 && onEmployeeChange ? ( ) : (

{employee.name}

)}
{/* Content */}
{error && (

{error}

)} {/* Тип пропуска (прогул/опоздание/ранний уход только для подчинённых — руководитель или HR) */}
{allowedTypes.map((type) => { const typeInfo = absenceTypeLabels[type]; return ( ); })}
{/* Тип отпуска (только для отпусков) */} {showVacationFields && (
)} {/* Даты */} {showDateRange && (
setStartDate(e.target.value)} required className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
{ setEndDate(e.target.value); if (absenceType === 'vacation') setVacationEndManuallyChanged(true); }} required={absenceType !== 'sick_leave'} className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" /> {absenceType === 'vacation' && (

По умолчанию +14 дней от начала, можно изменить

)} {absenceType === 'sick_leave' && (

Заполняется при выходе с больничного

)}
)} {/* Время (для опозданий и ранних уходов) */} {showTimeFields && (
setStartDate(e.target.value)} required className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
setStartTime(e.target.value)} required className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
setEndTime(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
)} {/* Дополнительные поля для больничных */} {showSickLeaveFields && (
setExpectedReturnDate(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" />

При выходе нажмите «Выход с больничного» и укажите номер листа

setSickLeaveNumber(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="Обязателен при выходе с больничного" />
setMedicalInstitution(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="Необязательно" />
setDiagnosis(e.target.value)} className="w-full px-4 py-3 rounded-xl border border-slate-200 bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="Необязательно" />
)} {/* Причина (для отгулов и прогулов) */} {(absenceType === 'day_off' || absenceType === 'absence' || absenceType === 'late' || absenceType === 'early_leave') && (