Files
mkd/components/hr/TrainingModule.tsx
2026-02-04 00:17:04 +05:00

779 lines
36 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};