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

265 lines
12 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 } from 'react';
import { X, CheckCircle2, XCircle, Play, Clock, UserCheck, Award, FileText } from 'lucide-react';
import { EmployeeTraining, TrainingStatus } from '../../types';
interface TrainingManagementModalProps {
training: EmployeeTraining;
onClose: () => void;
onUpdate: (trainingId: number, updates: Partial<EmployeeTraining>) => Promise<void>;
}
export const TrainingManagementModal: React.FC<TrainingManagementModalProps> = ({
training,
onClose,
onUpdate
}) => {
const [status, setStatus] = useState<TrainingStatus>(training.status || 'in_progress');
const [completionDate, setCompletionDate] = useState(training.completionDate || '');
const [score, setScore] = useState(training.score?.toString() || '');
const [passed, setPassed] = useState(training.passed ?? null);
const [certificateNumber, setCertificateNumber] = useState(training.certificateNumber || '');
const [notes, setNotes] = useState(training.notes || '');
const [saving, setSaving] = useState(false);
const handleStatusChange = (newStatus: TrainingStatus) => {
setStatus(newStatus);
// Автоматически устанавливаем дату завершения при статусе "completed"
if (newStatus === 'completed' && !completionDate) {
setCompletionDate(new Date().toISOString().split('T')[0]);
setPassed(true);
} else if (newStatus === 'failed') {
setPassed(false);
} else if (newStatus === 'cancelled') {
setPassed(null);
}
};
const handleSave = async () => {
try {
setSaving(true);
const updates: Partial<EmployeeTraining> = {
status,
completionDate: completionDate || undefined,
score: score ? parseFloat(score) : undefined,
passed,
certificateNumber: certificateNumber || undefined,
notes: notes || undefined
};
await onUpdate(training.id, updates);
onClose();
} catch (error) {
console.error('Error updating training:', error);
alert('Ошибка при обновлении обучения');
} finally {
setSaving(false);
}
};
const getStatusIcon = (status: TrainingStatus) => {
switch (status) {
case 'completed': return <CheckCircle2 className="w-4 h-4 text-emerald-600"/>;
case 'failed': return <XCircle className="w-4 h-4 text-red-600"/>;
case 'cancelled': return <X className="w-4 h-4 text-slate-400"/>;
case 'in_progress': return <Play className="w-4 h-4 text-amber-600"/>;
case 'not_started': return <Clock className="w-4 h-4 text-slate-400"/>;
default: return <Clock className="w-4 h-4"/>;
}
};
const getStatusLabel = (status: TrainingStatus) => {
const labels: Record<TrainingStatus, string> = {
'not_started': 'Не начато',
'in_progress': 'В процессе',
'completed': 'Завершено',
'failed': 'Не пройдено',
'expired': 'Просрочено',
'cancelled': 'Отменено'
};
return labels[status] || status;
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 bg-white border-b border-slate-200 p-6 rounded-t-2xl z-10">
<div className="flex justify-between items-center">
<div>
<h3 className="text-2xl font-bold text-slate-800">Управление обучением</h3>
<p className="text-sm text-slate-400 mt-1">
{training.programTitle} {training.employeeName}
</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-full transition-colors"
>
<X className="w-5 h-5 text-slate-500"/>
</button>
</div>
</div>
<div className="p-6 space-y-6">
{/* Статус обучения */}
<div>
<label className="block text-sm font-bold text-slate-700 mb-3">
Статус обучения
</label>
<div className="grid grid-cols-3 gap-2">
{(['in_progress', 'completed', 'failed', 'cancelled'] as TrainingStatus[]).map((s) => (
<button
key={s}
onClick={() => handleStatusChange(s)}
className={`flex items-center justify-center gap-2 p-3 rounded-xl border-2 transition-all ${
status === s
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-slate-200 hover:border-slate-300 text-slate-600'
}`}
>
{getStatusIcon(s)}
<span className="text-xs font-bold">{getStatusLabel(s)}</span>
</button>
))}
</div>
</div>
{/* Посещаемость (можно отметить в примечаниях) */}
<div className="bg-blue-50 p-4 rounded-xl border border-blue-100">
<p className="text-xs text-blue-700 font-bold mb-2 flex items-center gap-2">
<UserCheck className="w-4 h-4"/> Посещаемость
</p>
<p className="text-xs text-blue-600">
Отметьте посещаемость в примечаниях ниже (например: "Посетил все занятия" или "Не посетил 2 из 5 занятий")
</p>
</div>
{/* Детали завершения (показываются только для завершенных/не пройденных) */}
{(status === 'completed' || status === 'failed') && (
<>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Дата завершения
</label>
<input
type="date"
value={completionDate}
onChange={(e) => setCompletionDate(e.target.value)}
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Оценка (баллы)
</label>
<input
type="number"
min="0"
max="100"
value={score}
onChange={(e) => setScore(e.target.value)}
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="0-100"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Результат
</label>
<div className="flex gap-2">
<button
onClick={() => setPassed(true)}
className={`flex-1 flex items-center justify-center gap-2 p-3 rounded-xl border-2 transition-all ${
passed === true
? 'border-emerald-500 bg-emerald-50 text-emerald-700'
: 'border-slate-200 hover:border-slate-300 text-slate-600'
}`}
>
<CheckCircle2 className="w-4 h-4"/> Сдал
</button>
<button
onClick={() => setPassed(false)}
className={`flex-1 flex items-center justify-center gap-2 p-3 rounded-xl border-2 transition-all ${
passed === false
? 'border-red-500 bg-red-50 text-red-700'
: 'border-slate-200 hover:border-slate-300 text-slate-600'
}`}
>
<XCircle className="w-4 h-4"/> Не сдал
</button>
</div>
</div>
</div>
{status === 'completed' && (
<div>
<label className="block text-sm font-bold text-slate-700 mb-2 flex items-center gap-2">
<Award className="w-4 h-4"/> Номер сертификата
</label>
<input
type="text"
value={certificateNumber}
onChange={(e) => setCertificateNumber(e.target.value)}
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="СЕРТ-2024-001"
/>
</div>
)}
</>
)}
{/* Примечания */}
<div>
<label className="block text-sm font-bold text-slate-700 mb-2 flex items-center gap-2">
<FileText className="w-4 h-4"/> Примечания
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="Дополнительная информация об обучении..."
/>
</div>
{/* Информация о программе */}
<div className="bg-slate-50 p-4 rounded-xl">
<p className="text-xs text-slate-400 font-bold uppercase mb-2">Информация о программе</p>
<div className="space-y-1 text-sm">
<p><span className="font-bold">Начало:</span> {training.startDate ? new Date(training.startDate).toLocaleDateString('ru-RU') : 'Не указано'}</p>
{training.expiryDate && (
<p><span className="font-bold">Срок действия до:</span> {new Date(training.expiryDate).toLocaleDateString('ru-RU')}</p>
)}
{training.programDurationHours && (
<p><span className="font-bold">Длительность:</span> {training.programDurationHours} ч.</p>
)}
</div>
</div>
{/* Кнопки действий */}
<div className="flex gap-3 pt-4 border-t border-slate-200">
<button
onClick={onClose}
className="flex-1 px-6 py-3 border border-slate-200 text-slate-700 rounded-xl font-bold hover:bg-slate-50 transition-all"
>
Отмена
</button>
<button
onClick={handleSave}
disabled={saving}
className="flex-1 px-6 py-3 bg-primary-600 text-white rounded-xl font-bold hover:bg-primary-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? 'Сохранение...' : 'Сохранить изменения'}
</button>
</div>
</div>
</div>
</div>
);
};