Files
mkd/components/hr/TrainingModule.tsx

779 lines
36 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
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<Employee[]>([]);
const [programs, setPrograms] = useState<TrainingProgram[]>([]);
const [trainings, setTrainings] = useState<EmployeeTraining[]>([]);
const [loading, setLoading] = useState(true);
const [selectedEmployee, setSelectedEmployee] = useState<string | null>(null);
const [filterStatus, setFilterStatus] = useState<TrainingStatus | 'all'>('all');
const [filterCategory, setFilterCategory] = useState<TrainingCategory | 'all'>('all');
const [isProgramModalOpen, setIsProgramModalOpen] = useState(false);
const [editingProgram, setEditingProgram] = useState<TrainingProgram | null>(null);
const [managingTraining, setManagingTraining] = useState<EmployeeTraining | null>(null);
const [trainingForBooking, setTrainingForBooking] = useState<EmployeeTraining | null>(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<TrainingStatus, string> = {
not_started: 'Не начато',
in_progress: 'В процессе',
completed: 'Завершено',
failed: 'Не пройдено',
expired: 'Просрочено',
cancelled: 'Отменено'
};
return labels[status] || status;
};
const getCategoryLabel = (category: TrainingCategory) => {
const labels: Record<TrainingCategory, string> = {
safety: 'Техника безопасности',
fire_safety: 'Пожарная безопасность',
electrical: 'Электротехническая безопасность',
first_aid: 'Первая помощь',
professional: 'Профессиональное обучение',
compliance: 'Соответствие требованиям',
other: 'Другое'
};
return labels[category] || category;
};
const getTypeLabel = (type: TrainingType) => {
const labels: Record<TrainingType, string> = {
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<EmployeeTraining>) => {
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 (
<div className="flex items-center justify-center py-20">
<div className="text-slate-400 text-sm">Загрузка данных...</div>
</div>
);
}
return (
<div className="space-y-6 animate-fade-in">
{/* Статистика */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white p-6 rounded-[2rem] border border-slate-200 shadow-sm flex items-center gap-5">
<div className="w-16 h-16 rounded-3xl bg-emerald-50 flex items-center justify-center text-emerald-600">
<CheckCircle2 className="w-8 h-8"/>
</div>
<div>
<h4 className="text-2xl font-black text-slate-800 leading-none">{stats.completed}</h4>
<p className="text-[10px] text-slate-400 font-bold uppercase mt-1.5 tracking-wider">Завершено</p>
</div>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-amber-100 shadow-sm flex items-center gap-5">
<div className="w-16 h-16 rounded-3xl bg-amber-50 flex items-center justify-center text-amber-600">
<Clock className="w-8 h-8"/>
</div>
<div>
<h4 className="text-2xl font-black text-amber-600 leading-none">{stats.inProgress}</h4>
<p className="text-[10px] text-amber-400 font-bold uppercase mt-1.5 tracking-wider">В процессе</p>
</div>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-red-100 shadow-sm flex items-center gap-5 relative overflow-hidden">
<div className="absolute right-0 top-0 p-4 opacity-5">
<AlertTriangle className="w-20 h-20 text-red-600"/>
</div>
<div className="w-16 h-16 rounded-3xl bg-red-50 flex items-center justify-center text-red-600">
<AlertTriangle className="w-8 h-8 animate-pulse"/>
</div>
<div>
<h4 className="text-2xl font-black text-red-600 leading-none">{stats.expired}</h4>
<p className="text-[10px] text-red-400 font-bold uppercase mt-1.5 tracking-wider">Просрочено</p>
</div>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-200 shadow-sm flex items-center gap-5">
<div className="w-16 h-16 rounded-3xl bg-primary-50 flex items-center justify-center text-primary-600">
<BookOpen className="w-8 h-8"/>
</div>
<div>
<h4 className="text-2xl font-black text-slate-800 leading-none">{programs.length}</h4>
<p className="text-[10px] text-slate-400 font-bold uppercase mt-1.5 tracking-wider">Программ</p>
</div>
</div>
</div>
{/* Фильтры и действия */}
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-slate-400"/>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value as TrainingStatus | 'all')}
className="px-3 py-2 border border-slate-200 rounded-xl text-sm font-bold text-slate-700 bg-white"
>
<option value="all">Все статусы</option>
<option value="not_started">Не начато</option>
<option value="in_progress">В процессе</option>
<option value="completed">Завершено</option>
<option value="failed">Не пройдено</option>
<option value="expired">Просрочено</option>
</select>
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value as TrainingCategory | 'all')}
className="px-3 py-2 border border-slate-200 rounded-xl text-sm font-bold text-slate-700 bg-white"
>
<option value="all">Все категории</option>
<option value="safety">Техника безопасности</option>
<option value="fire_safety">Пожарная безопасность</option>
<option value="electrical">Электротехническая безопасность</option>
<option value="first_aid">Первая помощь</option>
<option value="professional">Профессиональное обучение</option>
<option value="compliance">Соответствие требованиям</option>
</select>
<select
value={selectedEmployee || 'all'}
onChange={(e) => setSelectedEmployee(e.target.value === 'all' ? null : e.target.value)}
className="px-3 py-2 border border-slate-200 rounded-xl text-sm font-bold text-slate-700 bg-white"
>
<option value="all">Все сотрудники</option>
{activeEmployees.map(emp => (
<option key={emp.id} value={emp.id}>{emp.name}</option>
))}
</select>
</div>
<div className="flex gap-2 ml-auto">
<button
onClick={() => {
setEditingProgram(null);
setIsProgramModalOpen(true);
}}
className="px-4 py-2 bg-primary-600 text-white rounded-xl text-[10px] font-black uppercase flex items-center gap-2 hover:bg-primary-700 transition-all"
>
<Plus className="w-4 h-4"/> Программа
</button>
</div>
</div>
{/* Список программ обучения */}
{programs.length > 0 && (
<div className="space-y-4 mb-6">
<div className="flex items-center justify-between px-1">
<div>
<h3 className="font-black text-slate-500 text-[10px] uppercase tracking-[0.2em]">
Программы обучения (шаблоны)
</h3>
<p className="text-[9px] text-slate-400 font-bold mt-1">
Создайте программу и назначьте её сотрудникам записи появятся ниже
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{programs.map(prog => (
<div
key={prog.id}
className="bg-white p-4 rounded-2xl border-2 border-slate-200 hover:border-primary-300 transition-all"
>
<div className="flex justify-between items-start mb-3">
<div className="flex-1">
<h4 className="text-sm font-black text-slate-800 leading-tight mb-1">
{prog.title}
</h4>
<p className="text-xs text-slate-400 font-bold uppercase">
{getTypeLabel(prog.type)} {getCategoryLabel(prog.category)}
</p>
</div>
<div className="flex gap-2">
<button
onClick={() => {
setEditingProgram(prog);
setIsProgramModalOpen(true);
}}
className="p-1.5 text-slate-400 hover:text-primary-600 transition-colors"
title="Редактировать"
>
<Edit className="w-4 h-4"/>
</button>
<button
onClick={() => {
setEditingProgram(prog);
setIsProgramModalOpen(true);
}}
className="p-1.5 text-slate-400 hover:text-emerald-600 transition-colors"
title="Записать сотрудников на программу"
>
<Users className="w-4 h-4"/>
</button>
</div>
</div>
{prog.description && (
<p className="text-xs text-slate-500 mb-2 line-clamp-2">{prog.description}</p>
)}
<div className="flex items-center gap-4 text-xs text-slate-400">
{prog.durationHours && (
<span className="flex items-center gap-1">
<Clock className="w-3 h-3"/> {prog.durationHours} ч.
</span>
)}
{prog.validityMonths && (
<span>Срок: {prog.validityMonths} мес.</span>
)}
{prog.isRequired && (
<span className="text-amber-600 font-bold">Обязательное</span>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Список обучений */}
<div className="space-y-4">
<div className="flex items-center justify-between px-1">
<div>
<h3 className="font-black text-slate-500 text-[10px] uppercase tracking-[0.2em]">
{selectedEmployee ? 'Обучение сотрудника' : 'Назначенные обучения'}
</h3>
<p className="text-[9px] text-slate-400 font-bold mt-1">
{selectedEmployee
? 'Обучение, назначенное выбранному сотруднику'
: 'Конкретные назначения программ сотрудникам. Назначьте программу выше, чтобы здесь появились записи'}
</p>
</div>
</div>
{filteredTrainings.length === 0 ? (
<div className="bg-white p-8 rounded-2xl border border-slate-100 text-center">
<BookOpen className="w-12 h-12 text-slate-300 mx-auto mb-3"/>
<p className="text-slate-400 text-sm font-bold mb-2">Нет назначенных обучений</p>
<p className="text-[10px] text-slate-400 font-bold">
{programs.length > 0
? 'Выберите программу выше и нажмите иконку 👥, чтобы назначить её сотрудникам'
: 'Создайте программу обучения, затем назначьте её сотрудникам'}
</p>
</div>
) : (
<div className="space-y-3">
{filteredTrainings.map(training => {
const isExpired = training.expiryDate && new Date(training.expiryDate) < new Date();
const isOverdue = isExpired && training.status === 'completed';
return (
<div
key={training.id}
className={`bg-white p-4 rounded-2xl border-2 transition-all ${
isOverdue ? 'border-red-200 bg-red-50/5' : getStatusColor(training.status, training.expiryDate)
}`}
>
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-3 flex-1">
<div className={`p-2.5 rounded-xl ${
isOverdue ? 'bg-red-100 text-red-600' :
training.status === 'completed' ? 'bg-emerald-100 text-emerald-600' :
'bg-slate-50 text-slate-400'
}`}>
{training.programCategory === 'safety' || training.programCategory === 'electrical' ? (
<HardHat className="w-5 h-5"/>
) : training.programCategory === 'fire_safety' ? (
<Zap className="w-5 h-5"/>
) : (
<GraduationCap className="w-5 h-5"/>
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-black text-slate-800 leading-tight">
{training.programTitle || 'Неизвестная программа'}
</p>
<div className="flex items-center gap-2 mt-1 flex-wrap">
<span className="text-[9px] text-slate-400 font-bold uppercase">
{training.employeeName || 'Неизвестный сотрудник'}
</span>
{training.employeePosition && (
<>
<span className="text-slate-300"></span>
<span className="text-[9px] text-slate-400 font-bold uppercase">
{training.employeePosition}
</span>
</>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
{isOverdue ? (
<span className="text-[8px] font-black bg-red-500 text-white px-2 py-0.5 rounded-full uppercase animate-pulse">
Пересдать!
</span>
) : training.status === 'completed' ? (
<div className="flex items-center gap-1 text-[9px] font-black text-emerald-500 uppercase">
<CheckCircle2 className="w-3 h-3"/> Завершено
</div>
) : (
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${
training.status === 'in_progress' ? 'bg-amber-100 text-amber-600' :
training.status === 'failed' ? 'bg-red-100 text-red-600' :
training.status === 'cancelled' ? 'bg-slate-100 text-slate-400' :
'bg-slate-100 text-slate-500'
}`}>
{getStatusLabel(training.status)}
</span>
)}
<button
onClick={() => setTrainingForBooking(training)}
className="p-1.5 text-slate-400 hover:text-amber-600 transition-colors"
title="Забронировать переговорную для обучения"
>
<Building2 className="w-4 h-4"/>
</button>
<button
onClick={() => setManagingTraining(training)}
className="p-1.5 text-slate-400 hover:text-primary-600 transition-colors"
title="Управление обучением"
>
<Edit className="w-4 h-4"/>
</button>
</div>
</div>
{training.location && (
<div className="flex items-center gap-2 mb-2 text-[9px] font-bold text-slate-500 uppercase">
<MapPin className="w-3.5 h-3.5 text-slate-400"/>
<span>Где: {training.location}</span>
</div>
)}
<div className="grid grid-cols-2 gap-4 border-t border-slate-100 pt-3">
<div className="flex items-center gap-2">
<FileText className="w-3.5 h-3.5 text-slate-400"/>
<span className="text-[9px] font-bold text-slate-500 uppercase">
{getTypeLabel(training.programType || 'other')} {getCategoryLabel(training.programCategory || 'other')}
</span>
</div>
<div className="flex items-center gap-2 text-right justify-end">
{training.expiryDate ? (
<>
<Clock className={`w-3.5 h-3.5 ${isExpired ? 'text-red-500' : 'text-slate-300'}`}/>
<span className={`text-[9px] font-bold uppercase ${isExpired ? 'text-red-600' : 'text-slate-500'}`}>
До: {new Date(training.expiryDate).toLocaleDateString('ru-RU')}
</span>
</>
) : training.completionDate ? (
<>
<Calendar className="w-3.5 h-3.5 text-slate-300"/>
<span className="text-[9px] font-bold text-slate-500 uppercase">
{new Date(training.completionDate).toLocaleDateString('ru-RU')}
</span>
</>
) : null}
</div>
</div>
{training.certificateNumber && (
<div className="mt-2 pt-2 border-t border-slate-100">
<div className="flex items-center gap-2">
<FileText className="w-3 h-3 text-slate-400"/>
<span className="text-[9px] font-bold text-slate-500">
Сертификат: {training.certificateNumber}
</span>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
{/* Кнопка формирования приказа */}
<button className="w-full py-4 bg-slate-900 text-white rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] shadow-xl active:scale-95 transition-all flex items-center justify-center gap-3">
<ClipboardList className="w-5 h-5"/> Сформировать приказ об инструктаже
</button>
{/* Модальное окно создания/редактирования программы */}
{isProgramModalOpen && (
<ProgramModal
program={editingProgram || undefined}
employees={activeEmployees}
onClose={() => {
setIsProgramModalOpen(false);
setEditingProgram(null);
}}
onSave={handleSaveProgram}
/>
)}
{/* Модальное окно управления обучением */}
{managingTraining && (
<TrainingManagementModal
training={managingTraining}
onClose={() => setManagingTraining(null)}
onUpdate={handleUpdateTraining}
/>
)}
{/* Бронирование переговорной для обучения */}
{trainingForBooking && (
<BookMeetingRoomModal
purpose={`Обучение: ${trainingForBooking.programTitle || 'Программа'}${trainingForBooking.employeeName || 'Сотрудник'}`}
defaultDate={(() => {
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)}
/>
)}
</div>
);
};