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

401 lines
18 KiB
TypeScript
Executable File
Raw Permalink 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 { X, Users, Check, Search } from 'lucide-react';
import { TrainingProgram, TrainingType, TrainingCategory, Employee } from '../../types';
interface ProgramModalProps {
program?: TrainingProgram | null;
employees?: Employee[];
onClose: () => void;
onSave: (program: TrainingProgram, selectedEmployeeIds?: string[]) => void;
}
export const ProgramModal: React.FC<ProgramModalProps> = ({ program, employees = [], onClose, onSave }) => {
const [formData, setFormData] = useState({
title: '',
description: '',
type: 'instruction' as TrainingType,
category: 'safety' as TrainingCategory,
durationHours: '',
validityMonths: '',
isRequired: false,
requiredForPositions: '',
instructorName: '',
materialsUrl: ''
});
const [selectedEmployees, setSelectedEmployees] = useState<string[]>([]);
const [showEmployeeSelection, setShowEmployeeSelection] = useState(false);
const [employeeSearchQuery, setEmployeeSearchQuery] = useState('');
useEffect(() => {
if (program) {
setFormData({
title: program.title || '',
description: program.description || '',
type: program.type || 'instruction',
category: program.category || 'safety',
durationHours: program.durationHours?.toString() || '',
validityMonths: program.validityMonths?.toString() || '',
isRequired: program.isRequired || false,
requiredForPositions: program.requiredForPositions?.join(', ') || '',
instructorName: program.instructorName || '',
materialsUrl: program.materialsUrl || ''
});
// При редактировании существующей программы показываем выбор сотрудников
setShowEmployeeSelection(true);
} else {
// При создании новой программы выбор сотрудников скрыт по умолчанию
setShowEmployeeSelection(false);
setSelectedEmployees([]);
setEmployeeSearchQuery('');
}
}, [program]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.title.trim()) {
alert('Название программы обязательно');
return;
}
const programData: TrainingProgram = {
// ID только для существующих программ, для новых - undefined (backend сгенерирует)
id: program?.id || undefined,
title: formData.title.trim(),
description: formData.description.trim() || undefined,
type: formData.type,
category: formData.category,
durationHours: formData.durationHours ? parseFloat(formData.durationHours) : undefined,
validityMonths: formData.validityMonths ? parseInt(formData.validityMonths) : undefined,
isRequired: formData.isRequired,
requiredForPositions: formData.requiredForPositions
? formData.requiredForPositions.split(',').map(p => p.trim()).filter(p => p)
: undefined,
instructorName: formData.instructorName.trim() || undefined,
materialsUrl: formData.materialsUrl.trim() || undefined
};
// Передаем выбранных сотрудников для массового назначения
onSave(programData, selectedEmployees.length > 0 ? selectedEmployees : undefined);
};
const toggleEmployee = (employeeId: string) => {
setSelectedEmployees(prev =>
prev.includes(employeeId)
? prev.filter(id => id !== employeeId)
: [...prev, employeeId]
);
};
// Фильтрация сотрудников по поисковому запросу
const filteredEmployees = useMemo(() => {
if (!employeeSearchQuery.trim()) {
return employees;
}
const query = employeeSearchQuery.toLowerCase().trim();
return employees.filter(emp =>
emp.name?.toLowerCase().includes(query) ||
emp.position?.toLowerCase().includes(query)
);
}, [employees, employeeSearchQuery]);
const selectAll = () => {
if (selectedEmployees.length === filteredEmployees.length) {
// Снимаем только выбранных из отфильтрованного списка
setSelectedEmployees(prev =>
prev.filter(id => !filteredEmployees.some(emp => emp.id === id))
);
} else {
// Добавляем всех из отфильтрованного списка
const filteredIds = filteredEmployees.map(emp => emp.id);
setSelectedEmployees(prev => {
const newIds = [...new Set([...prev, ...filteredIds])];
return newIds;
});
}
};
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in"
onClick={onClose}
>
<div
className="bg-white rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl animate-slide-up"
onClick={e => e.stopPropagation()}
>
<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">
<h3 className="text-2xl font-bold text-slate-800">
{program ? `Редактировать программу: ${program.title}` : 'Создать программу обучения'}
</h3>
<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>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Название программы *
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: 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"
required
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Описание
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: 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"
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Тип обучения *
</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value as TrainingType })}
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary-500"
required
>
<option value="instruction">Инструктаж</option>
<option value="course">Курс</option>
<option value="certification">Сертификация</option>
<option value="exam">Экзамен</option>
<option value="other">Другое</option>
</select>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Категория *
</label>
<select
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value as TrainingCategory })}
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary-500"
required
>
<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>
<option value="other">Другое</option>
</select>
</div>
</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"
step="0.5"
min="0"
value={formData.durationHours}
onChange={(e) => setFormData({ ...formData, durationHours: 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>
<label className="block text-sm font-bold text-slate-700 mb-2">
Срок действия (месяцев)
</label>
<input
type="number"
min="0"
value={formData.validityMonths}
onChange={(e) => setFormData({ ...formData, validityMonths: 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="Пусто = бессрочно"
/>
</div>
</div>
<div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={formData.isRequired}
onChange={(e) => setFormData({ ...formData, isRequired: e.target.checked })}
className="w-4 h-4 text-primary-600 rounded focus:ring-primary-500"
/>
<span className="text-sm font-bold text-slate-700">Обязательное обучение</span>
</label>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Должности (через запятую)
</label>
<input
type="text"
value={formData.requiredForPositions}
onChange={(e) => setFormData({ ...formData, requiredForPositions: 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="Слесарь, Электрик, Мастер"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Инструктор/Преподаватель
</label>
<input
type="text"
value={formData.instructorName}
onChange={(e) => setFormData({ ...formData, instructorName: 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>
<label className="block text-sm font-bold text-slate-700 mb-2">
Ссылка на материалы
</label>
<input
type="url"
value={formData.materialsUrl}
onChange={(e) => setFormData({ ...formData, materialsUrl: 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="https://..."
/>
</div>
{/* Выбор сотрудников для массового назначения */}
<div className="border-t border-slate-200 pt-6">
<div className="flex items-center justify-between mb-4">
<div>
<h4 className="text-sm font-bold text-slate-700 mb-1 flex items-center gap-2">
<Users className="w-4 h-4"/> Назначить сотрудникам
</h4>
<p className="text-xs text-slate-400">
{selectedEmployees.length > 0
? `Выбрано: ${selectedEmployees.length} сотрудников`
: program
? 'Выберите сотрудников для записи на эту программу'
: 'Выберите сотрудников для автоматического назначения программы'}
</p>
</div>
<button
type="button"
onClick={() => setShowEmployeeSelection(!showEmployeeSelection)}
className="px-4 py-2 text-sm font-bold text-primary-600 hover:bg-primary-50 rounded-xl transition-all"
>
{showEmployeeSelection ? 'Скрыть' : 'Выбрать'}
</button>
</div>
{showEmployeeSelection && employees.length > 0 && (
<div className="max-h-60 overflow-y-auto border border-slate-200 rounded-xl p-4 space-y-2">
{/* Поиск сотрудников */}
<div className="mb-3 pb-3 border-b border-slate-200">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400"/>
<input
type="text"
value={employeeSearchQuery}
onChange={(e) => setEmployeeSearchQuery(e.target.value)}
placeholder="Поиск по имени или должности..."
className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
/>
</div>
</div>
{filteredEmployees.length > 0 && (
<div className="flex items-center justify-between mb-3 pb-3 border-b border-slate-200">
<button
type="button"
onClick={selectAll}
className="text-xs font-bold text-primary-600 hover:text-primary-700"
>
{filteredEmployees.every(emp => selectedEmployees.includes(emp.id))
? 'Снять все'
: 'Выбрать всех'}
</button>
<span className="text-xs text-slate-400">
{selectedEmployees.filter(id => filteredEmployees.some(emp => emp.id === id)).length} из {filteredEmployees.length}
</span>
</div>
)}
{filteredEmployees.length > 0 ? (
filteredEmployees.map(emp => (
<label
key={emp.id}
className="flex items-center gap-3 p-2 hover:bg-slate-50 rounded-lg cursor-pointer"
>
<input
type="checkbox"
checked={selectedEmployees.includes(emp.id)}
onChange={() => toggleEmployee(emp.id)}
className="w-4 h-4 text-primary-600 rounded focus:ring-primary-500"
/>
<div className="flex-1">
<p className="text-sm font-bold text-slate-800">{emp.name}</p>
<p className="text-xs text-slate-400">{emp.position}</p>
</div>
</label>
))
) : (
<div className="text-center py-4 text-slate-400 text-sm">
Сотрудники не найдены
</div>
)}
</div>
)}
{showEmployeeSelection && employees.length === 0 && (
<div className="text-center py-8 text-slate-400 text-sm border border-slate-200 rounded-xl">
Нет доступных сотрудников
</div>
)}
</div>
<div className="flex gap-3 pt-4">
<button
type="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
type="submit"
className="flex-1 px-6 py-3 bg-primary-600 text-white rounded-xl font-bold hover:bg-primary-700 transition-all flex items-center justify-center gap-2"
>
{program ? 'Сохранить' : 'Создать'}
{selectedEmployees.length > 0 && (
<span className="text-xs opacity-90 bg-white/20 px-2 py-0.5 rounded-full">
{selectedEmployees.length} сотрудников
</span>
)}
</button>
</div>
</form>
</div>
</div>
);
};