import React, { useState, useEffect, useMemo } from 'react'; import { ShieldCheck, AlertTriangle, CheckCircle2, ClipboardList, HardHat, Zap, Clock, BookOpen, GraduationCap, Plus, Edit, Trash2, X, FileText, Calendar, User, Search, Filter, Users, Settings, Building2, MapPin } from 'lucide-react'; import { Employee, TrainingProgram, EmployeeTraining, TrainingStatus, TrainingType, TrainingCategory } from '../../types'; import { ProgramModal } from './ProgramModal'; import { TrainingManagementModal } from './TrainingManagementModal'; import { BookMeetingRoomModal } from '../office/BookMeetingRoomModal'; import { authFetch } from '../../services/apiClient'; export const TrainingModule: React.FC = () => { const [employees, setEmployees] = useState([]); const [programs, setPrograms] = useState([]); const [trainings, setTrainings] = useState([]); const [loading, setLoading] = useState(true); const [selectedEmployee, setSelectedEmployee] = useState(null); const [filterStatus, setFilterStatus] = useState('all'); const [filterCategory, setFilterCategory] = useState('all'); const [isProgramModalOpen, setIsProgramModalOpen] = useState(false); const [editingProgram, setEditingProgram] = useState(null); const [managingTraining, setManagingTraining] = useState(null); const [trainingForBooking, setTrainingForBooking] = useState(null); const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; const activeEmployees = useMemo( () => employees.filter((e) => e.status !== 'inactive'), [employees] ); useEffect(() => { fetchData(); }, []); const fetchData = async () => { try { setLoading(true); // Загружаем сотрудников const employeesUrl = (import.meta.env.DEV || !apiBaseUrl) ? '/api/employees' : `${apiBaseUrl}/employees`; const employeesRes = await authFetch(employeesUrl); if (employeesRes.ok) { const employeesData = await employeesRes.json(); setEmployees(employeesData); } // Загружаем программы обучения try { const programsUrl = (import.meta.env.DEV || !apiBaseUrl) ? '/api/training/programs' : `${apiBaseUrl}/training/programs`; const programsRes = await authFetch(programsUrl); if (programsRes.ok) { const programsData = await programsRes.json(); setPrograms(programsData); } else if (programsRes.status === 404) { // Endpoint еще не создан - это нормально, просто пустой список console.warn('Training programs endpoint not found - server may need restart'); setPrograms([]); } } catch (error) { console.warn('Error fetching training programs:', error); setPrograms([]); } // Загружаем все обучения await fetchTrainings(); } catch (error) { console.error('Error fetching data:', error); } finally { setLoading(false); } }; const fetchTrainings = async () => { try { if (selectedEmployee) { const trainingsUrl = (import.meta.env.DEV || !apiBaseUrl) ? `/api/training/employee/${selectedEmployee}` : `${apiBaseUrl}/training/employee/${selectedEmployee}`; const trainingsRes = await authFetch(trainingsUrl); if (trainingsRes.ok) { const trainingsData = await trainingsRes.json(); setTrainings(trainingsData); } else if (trainingsRes.status === 404) { console.warn('Training endpoint not found - server may need restart'); setTrainings([]); } } else { // Загружаем все назначенные обучения для всех сотрудников try { const allTrainingsUrl = (import.meta.env.DEV || !apiBaseUrl) ? '/api/training/all' : `${apiBaseUrl}/training/all`; const allTrainingsRes = await authFetch(allTrainingsUrl); if (allTrainingsRes.ok) { const allTrainingsData = await allTrainingsRes.json(); setTrainings(allTrainingsData || []); } else if (allTrainingsRes.status === 404) { // Если endpoint не найден, пробуем загрузить просроченные console.warn('Training all endpoint not found, trying overdue...'); const overdueUrl = (import.meta.env.DEV || !apiBaseUrl) ? '/api/training/overdue' : `${apiBaseUrl}/training/overdue`; const overdueRes = await authFetch(overdueUrl); if (overdueRes.ok) { const overdueData = await overdueRes.json(); setTrainings(overdueData || []); } else { setTrainings([]); } } } catch (error) { console.warn('Error fetching all trainings:', error); setTrainings([]); } } } catch (error) { console.error('Error fetching trainings:', error); setTrainings([]); } }; useEffect(() => { fetchTrainings(); }, [selectedEmployee]); // Статистика const stats = { total: trainings.length, completed: trainings.filter(t => t.status === 'completed').length, inProgress: trainings.filter(t => t.status === 'in_progress').length, expired: trainings.filter(t => { if (!t.expiryDate) return false; return new Date(t.expiryDate) < new Date() && t.status === 'completed'; }).length, overdue: trainings.filter(t => { if (!t.expiryDate) return false; return new Date(t.expiryDate) < new Date(); }).length }; // Фильтрация const filteredTrainings = trainings.filter(t => { if (filterStatus !== 'all' && t.status !== filterStatus) return false; if (filterCategory !== 'all' && t.programCategory !== filterCategory) return false; return true; }); const getStatusColor = (status: TrainingStatus, expiryDate?: string) => { if (expiryDate && new Date(expiryDate) < new Date() && status === 'completed') { return 'bg-red-50 text-red-600 border-red-200'; } switch (status) { case 'completed': return 'bg-emerald-50 text-emerald-600 border-emerald-200'; case 'in_progress': return 'bg-amber-50 text-amber-600 border-amber-200'; case 'failed': return 'bg-red-50 text-red-600 border-red-200'; case 'expired': return 'bg-red-50 text-red-600 border-red-200'; case 'cancelled': return 'bg-slate-50 text-slate-600 border-slate-200'; default: return 'bg-slate-50 text-slate-400 border-slate-200'; } }; const getStatusLabel = (status: TrainingStatus) => { const labels: Record = { not_started: 'Не начато', in_progress: 'В процессе', completed: 'Завершено', failed: 'Не пройдено', expired: 'Просрочено', cancelled: 'Отменено' }; return labels[status] || status; }; const getCategoryLabel = (category: TrainingCategory) => { const labels: Record = { safety: 'Техника безопасности', fire_safety: 'Пожарная безопасность', electrical: 'Электротехническая безопасность', first_aid: 'Первая помощь', professional: 'Профессиональное обучение', compliance: 'Соответствие требованиям', other: 'Другое' }; return labels[category] || category; }; const getTypeLabel = (type: TrainingType) => { const labels: Record = { instruction: 'Инструктаж', course: 'Курс', certification: 'Сертификация', exam: 'Экзамен', other: 'Другое' }; return labels[type] || type; }; const handleSaveProgram = async (program: TrainingProgram, selectedEmployeeIds?: string[]) => { try { // Определяем, создаем новую программу или редактируем существующую // Проверяем, что program существует и имеет валидный ID const isEdit = !!(program && program.id && typeof program.id === 'string' && program.id.trim() !== ''); const url = (import.meta.env.DEV || !apiBaseUrl) ? (isEdit ? `/api/training/programs/${program.id}` : '/api/training/programs') : (isEdit ? `${apiBaseUrl}/training/programs/${program.id}` : `${apiBaseUrl}/training/programs`); const method = isEdit ? 'PUT' : 'POST'; // Преобразуем данные в формат, который ожидает backend // При создании новой программы НЕ отправляем id (backend сам сгенерирует) const requestData: any = { title: program.title, description: program.description, type: program.type, category: program.category, durationHours: program.durationHours, validityMonths: program.validityMonths, isRequired: program.isRequired, requiredForPositions: program.requiredForPositions, instructorName: program.instructorName, materialsUrl: program.materialsUrl }; // Добавляем id только при редактировании существующей программы if (isEdit) { requestData.id = program.id; } const response = await authFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }); if (response.ok) { const savedProgram = await response.json(); // Проверяем, что программа была успешно создана/обновлена if (!savedProgram) { alert('Ошибка: Программа не была создана. Проверьте данные и попробуйте снова.'); return; } // Получаем ID программы (может быть в разных форматах после нормализации) const programId = savedProgram.id; if (!programId) { console.error('Saved program response:', savedProgram); alert('Ошибка: Не удалось получить ID созданной программы. Проверьте консоль для деталей.'); return; } // Если выбраны сотрудники, назначаем им обучение массово if (selectedEmployeeIds && selectedEmployeeIds.length > 0) { // Небольшая задержка, чтобы убедиться, что программа сохранена в БД await new Promise(resolve => setTimeout(resolve, 200)); const assigned = await assignTrainingToEmployees(programId, selectedEmployeeIds); if (assigned > 0) { if (isEdit) { alert(`Программа обновлена. Обучение назначено ${assigned} сотрудникам`); } else { alert(`Программа создана и назначена ${assigned} сотрудникам`); } } else { if (isEdit) { alert('Программа обновлена, но не удалось назначить обучение сотрудникам. Проверьте данные.'); } else { alert('Программа создана, но не удалось назначить обучение сотрудникам. Проверьте данные.'); } } } else if (isEdit) { // Если редактируем программу без выбора сотрудников, просто сохраняем alert('Программа обновлена'); } else { alert('Программа успешно создана'); } setIsProgramModalOpen(false); setEditingProgram(null); await fetchData(); } else { let errorMessage = `Ошибка ${response.status}: ${response.statusText}`; try { const error = await response.json(); errorMessage = error.error || error.details || errorMessage; } catch (e) { const text = await response.text().catch(() => ''); if (text) errorMessage = text; } alert(`Ошибка: ${errorMessage}`); } } catch (error) { console.error('Error saving program:', error); alert('Ошибка при сохранении программы'); } }; const handleUpdateTraining = async (trainingId: number, updates: Partial) => { try { const training = trainings.find(t => t.id === trainingId); if (!training) { throw new Error('Обучение не найдено'); } const url = (import.meta.env.DEV || !apiBaseUrl) ? `/api/training/employee/${training.employeeId}/${trainingId}` : `${apiBaseUrl}/training/employee/${training.employeeId}/${trainingId}`; const response = await authFetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Не удалось обновить обучение'); } // Обновляем список обучений await fetchTrainings(); } catch (error) { console.error('Error updating training:', error); throw error; } }; const assignTrainingToEmployees = async (programId: string, employeeIds: string[]) => { const startDate = new Date().toISOString().split('T')[0]; let assignedCount = 0; const errors: string[] = []; for (const employeeId of employeeIds) { try { const url = (import.meta.env.DEV || !apiBaseUrl) ? `/api/training/employee/${employeeId}` : `${apiBaseUrl}/training/employee/${employeeId}`; const response = await authFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ programId, startDate }) }); if (response.ok) { assignedCount++; } else { const errorData = await response.json().catch(() => ({ error: response.statusText })); const errorMsg = errorData.error || `Ошибка ${response.status}`; errors.push(`Сотрудник ${employeeId}: ${errorMsg}`); console.error(`Error assigning training to employee ${employeeId}:`, errorMsg); } } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Неизвестная ошибка'; errors.push(`Сотрудник ${employeeId}: ${errorMsg}`); console.error(`Error assigning training to employee ${employeeId}:`, error); } } if (errors.length > 0 && assignedCount === 0) { console.error('Все назначения завершились ошибкой:', errors); } return assignedCount; }; if (loading) { return (
Загрузка данных...
); } return (
{/* Статистика */}

{stats.completed}

Завершено

{stats.inProgress}

В процессе

{stats.expired}

Просрочено

{programs.length}

Программ

{/* Фильтры и действия */}
{/* Список программ обучения */} {programs.length > 0 && (

Программы обучения (шаблоны)

Создайте программу и назначьте её сотрудникам — записи появятся ниже

{programs.map(prog => (

{prog.title}

{getTypeLabel(prog.type)} • {getCategoryLabel(prog.category)}

{prog.description && (

{prog.description}

)}
{prog.durationHours && ( {prog.durationHours} ч. )} {prog.validityMonths && ( Срок: {prog.validityMonths} мес. )} {prog.isRequired && ( Обязательное )}
))}
)} {/* Список обучений */}

{selectedEmployee ? 'Обучение сотрудника' : 'Назначенные обучения'}

{selectedEmployee ? 'Обучение, назначенное выбранному сотруднику' : 'Конкретные назначения программ сотрудникам. Назначьте программу выше, чтобы здесь появились записи'}

{filteredTrainings.length === 0 ? (

Нет назначенных обучений

{programs.length > 0 ? 'Выберите программу выше и нажмите иконку 👥, чтобы назначить её сотрудникам' : 'Создайте программу обучения, затем назначьте её сотрудникам'}

) : (
{filteredTrainings.map(training => { const isExpired = training.expiryDate && new Date(training.expiryDate) < new Date(); const isOverdue = isExpired && training.status === 'completed'; return (
{training.programCategory === 'safety' || training.programCategory === 'electrical' ? ( ) : training.programCategory === 'fire_safety' ? ( ) : ( )}

{training.programTitle || 'Неизвестная программа'}

{training.employeeName || 'Неизвестный сотрудник'} {training.employeePosition && ( <> {training.employeePosition} )}
{isOverdue ? ( Пересдать! ) : training.status === 'completed' ? (
Завершено
) : ( {getStatusLabel(training.status)} )}
{training.location && (
Где: {training.location}
)}
{getTypeLabel(training.programType || 'other')} • {getCategoryLabel(training.programCategory || 'other')}
{training.expiryDate ? ( <> До: {new Date(training.expiryDate).toLocaleDateString('ru-RU')} ) : training.completionDate ? ( <> {new Date(training.completionDate).toLocaleDateString('ru-RU')} ) : null}
{training.certificateNumber && (
Сертификат: {training.certificateNumber}
)}
); })}
)}
{/* Кнопка формирования приказа */} {/* Модальное окно создания/редактирования программы */} {isProgramModalOpen && ( { setIsProgramModalOpen(false); setEditingProgram(null); }} onSave={handleSaveProgram} /> )} {/* Модальное окно управления обучением */} {managingTraining && ( setManagingTraining(null)} onUpdate={handleUpdateTraining} /> )} {/* Бронирование переговорной для обучения */} {trainingForBooking && ( { try { const d = trainingForBooking.startDate; if (d == null || d === '') return undefined; const parsed = new Date(d); return isNaN(parsed.getTime()) ? undefined : parsed.toISOString().split('T')[0]; } catch { return undefined; } })()} onBooked={async (roomName) => { if (roomName) { try { await handleUpdateTraining(trainingForBooking.id, { location: roomName }); } catch (e) { console.error(e); } } setTrainingForBooking(null); }} onClose={() => setTrainingForBooking(null)} /> )}
); };