import React, { useState, useEffect, useMemo, useRef } from 'react'; import { UsersRound, UserPlus, ShieldCheck, Printer, Briefcase, Calendar, ChevronDown, FileText, Upload, Loader2 } from 'lucide-react'; import { Employee, User } from '../types'; import { backendApi, authFetch } from '../services/apiClient'; // Modular Imports import { HRSummary } from './hr/HRSummary'; import { EmployeeRegistry } from './hr/EmployeeRegistry'; import { HiringPipeline } from './hr/HiringPipeline'; import { VacanciesRegistry } from './hr/VacanciesRegistry'; import { CandidatesRegistry } from './hr/CandidatesRegistry'; import { TrainingModule } from './hr/TrainingModule'; import { EmployeeFormModal } from './hr/EmployeeFormModal'; import { WorkCalendarView } from './hr/WorkCalendarView'; import { allowedSubsForSection } from '../constants/permissions'; type Tab = 'summary' | 'employees' | 'vacancies' | 'hiring' | 'safety' | 'calendar'; const HR_TABS: Tab[] = ['summary', 'employees', 'calendar', 'vacancies', 'hiring', 'safety']; const SUBTAB_KEY = 'mkd_subTab_hr'; interface HRModuleProps { currentUser?: User; allowedPermissions?: string[] | null; } export const HRModule: React.FC = ({ currentUser, allowedPermissions }) => { const visibleTabs = useMemo(() => { const allowed = allowedSubsForSection(allowedPermissions ?? [], 'hr'); if (allowed === 'all') return HR_TABS; return HR_TABS.filter((t) => allowed.includes(t)); }, [allowedPermissions]); const [activeTab, setActiveTab] = useState(() => { const s = localStorage.getItem(SUBTAB_KEY); return (s && HR_TABS.includes(s as Tab)) ? s as Tab : 'summary'; }); useEffect(() => { if (visibleTabs.length > 0 && !visibleTabs.includes(activeTab)) { setActiveTab(visibleTabs[0]); } }, [visibleTabs, activeTab]); useEffect(() => { if (visibleTabs.includes(activeTab)) localStorage.setItem(SUBTAB_KEY, activeTab); }, [activeTab, visibleTabs]); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const [printerDropdownOpen, setPrinterDropdownOpen] = useState(false); const [templateDocs, setTemplateDocs] = useState<{ id: number; name: string; filePath: string; originalFilename: string | null; createdAt: string }[]>([]); const [loadingTemplates, setLoadingTemplates] = useState(false); const [uploadingTemplate, setUploadingTemplate] = useState(false); const printerDropdownRef = useRef(null); const fileInputRef = useRef(null); useEffect(() => { if (printerDropdownOpen) { setLoadingTemplates(true); backendApi.getHrTemplateDocuments() .then(setTemplateDocs) .catch(() => setTemplateDocs([])) .finally(() => setLoadingTemplates(false)); } }, [printerDropdownOpen]); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (printerDropdownRef.current && !printerDropdownRef.current.contains(e.target as Node)) { setPrinterDropdownOpen(false); } }; if (printerDropdownOpen) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [printerDropdownOpen]); const handleDownloadTemplate = async (doc: { id: number; name: string; originalFilename: string | null }) => { const url = backendApi.getHrTemplateDocumentDownloadUrl(doc.id); const res = await authFetch(url); const blob = await res.blob(); const u = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = u; a.download = doc.originalFilename || doc.name || 'document'; a.click(); URL.revokeObjectURL(u); }; const handleUploadTemplate = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setUploadingTemplate(true); const formData = new FormData(); formData.append('file', file); formData.append('name', file.name); backendApi.uploadHrTemplateDocument(formData) .then(() => backendApi.getHrTemplateDocuments().then(setTemplateDocs)) .catch((err) => console.error(err)) .finally(() => { setUploadingTemplate(false); e.target.value = ''; }); }; return (

Управление персоналом

Кадровая политика и штатное расписание

{printerDropdownOpen && (

Типовые документы

{loadingTemplates ? (
) : templateDocs.length === 0 ? (

Нет типовых документов

) : (
    {templateDocs.map((doc) => (
  • ))}
)}
)}
{/* Sheet Selector */}
{[ { id: 'summary', label: 'Сводка', icon: UsersRound }, { id: 'employees', label: 'Штат', icon: UsersRound }, { id: 'calendar', label: 'Рабочий календарь', icon: Calendar }, { id: 'vacancies', label: 'Вакансии', icon: Briefcase }, { id: 'hiring', label: 'Кандидаты', icon: UserPlus }, { id: 'safety', label: 'Обучение', icon: ShieldCheck }, ].filter((tab) => visibleTabs.includes(tab.id as Tab)).map((tab) => ( ))}
{/* Dynamic Sheet Content */}
{activeTab === 'summary' && } {activeTab === 'employees' && ( { setIsCreateModalOpen(false); }} currentUser={currentUser} onOpenCreateModal={() => setIsCreateModalOpen(true)} /> )} {activeTab === 'calendar' && } {activeTab === 'vacancies' && } {activeTab === 'hiring' && } {activeTab === 'safety' && }
{/* Форма создания сотрудника */} {isCreateModalOpen && ( setIsCreateModalOpen(false)} onSave={async (employee) => { // EmployeeFormModal уже создает сотрудника через API, // поэтому здесь просто закрываем модальное окно и обновляем список setIsCreateModalOpen(false); setRefreshKey(prev => prev + 1); // Обновляем список сотрудников }} /> )}
); };