import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { UsersRound, UserPlus, Gift, TrendingDown, GraduationCap, Plane, FileWarning, Calendar, ClipboardList } from 'lucide-react'; import { authFetch } from '../../services/apiClient'; import { readCache, saveCache } from '../../hooks/useCachedFetch'; import { REFRESH_EVENTS } from '../../constants/refreshEvents'; const CACHE_KEY = 'mkd_hr_summary_cache'; interface HRSummaryData { totalStaff: number; vacancyCount: number; activeCandidates: number; turnoverRate: number; onTraining: number; staffingByDistrict: { districtId: string; districtName: string; current: number; total: number }[]; events: { birthdays: { employeeId: string; name: string; initials: string; date: string; label: string }[]; vacationReturns: { employeeId: string; name: string; initials: string; date: string; label: string }[]; }; mostFrequentVacation: { employeeId: string; name: string; initials: string; count: number }[]; mostFrequentAbsences: { employeeId: string; name: string; initials: string; dayOff: number; absence: number; late: number; earlyLeave: number; sick: number; total: number; }[]; } interface Props { onNavigate: (tab: any) => void; } const defaultSummary: HRSummaryData = { totalStaff: 0, vacancyCount: 0, activeCandidates: 0, turnoverRate: 0, onTraining: 0, staffingByDistrict: [], events: { birthdays: [], vacationReturns: [] }, mostFrequentVacation: [], mostFrequentAbsences: [] }; export const HRSummary: React.FC = ({ onNavigate }) => { const cached = readCache(CACHE_KEY, null); const [summary, setSummary] = useState(cached || defaultSummary); const [loading, setLoading] = useState(!cached); const load = useCallback(async (showSpinner = true) => { if (showSpinner && !cached) setLoading(true); try { const response = await authFetch('/api/hr/summary'); if (response.ok) { const data = await response.json(); setSummary(data); saveCache(CACHE_KEY, data); } else setSummary(defaultSummary); } catch (error) { console.error('Error fetching HR summary:', error); setSummary(defaultSummary); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, []); useEffect(() => { const onRefresh = () => load(false); window.addEventListener(REFRESH_EVENTS.employees, onRefresh); return () => window.removeEventListener(REFRESH_EVENTS.employees, onRefresh); }, [load]); useEffect(() => { const interval = setInterval(() => load(false), 60 * 1000); return () => clearInterval(interval); }, [load]); const [holidaysRu, setHolidaysRu] = useState<{ date: string; name: string }[]>([]); const [holidayYear, setHolidayYear] = useState(() => new Date().getFullYear()); useEffect(() => { authFetch(`/api/holidays/ru?year=${holidayYear}`) .then((r) => r.json()) .then((data) => setHolidaysRu(data.holidays || [])) .catch(() => setHolidaysRu([])); }, [holidayYear]); const eventsList = [ ...summary.events.birthdays.map((e) => ({ ...e, type: 'birthday' as const })), ...summary.events.vacationReturns.map((e) => ({ ...e, type: 'vacation' as const })) ].slice(0, 10); const sickAndDayOff = [...summary.mostFrequentAbsences] .filter((e) => e.dayOff > 0 || e.sick > 0) .sort((a, b) => b.dayOff + b.sick - (a.dayOff + a.sick)) .slice(0, 10); const disciplinary = [...summary.mostFrequentAbsences] .filter((e) => e.absence > 0 || e.late > 0 || e.earlyLeave > 0) .sort((a, b) => b.absence + b.late + b.earlyLeave - (a.absence + a.late + a.earlyLeave)) .slice(0, 10); return (
{loading && (
)} {!loading && ( <> {/* KPI Grid */}
onNavigate('employees')} /> onNavigate('hiring')} /> onNavigate('safety')} />
{/* Events Widget */}

События недели

{eventsList.length === 0 && (

Нет предстоящих событий

)} {eventsList.map((e) => { const eventSubtitle = e.type === 'birthday' && (e.label === 'Сегодня' || e.label === 'Завтра') ? `День рождения • ${e.label}` : e.type === 'birthday' ? `День рождения • ${e.label.replace(/^День рождения •\s*/i, '')}` : e.label; return (
{e.initials}

{e.name}

{eventSubtitle}

); })}
{/* Календарь праздников РФ */}
{/* Самые частые в отпуске / больничные и отгулы / дисциплинарные нарушения */}

Самые частые в отпуске

{summary.mostFrequentVacation.length === 0 && (

Нет данных

)}
    {summary.mostFrequentVacation.map((e) => (
  • {e.initials}
    {e.name}
    {e.count} отпуск{e.count === 1 ? '' : e.count < 5 ? 'а' : 'ов'}
  • ))}

Больничные и отгулы

{sickAndDayOff.length === 0 && (

Нет данных

)}
    {sickAndDayOff.map((e) => (
  • {e.initials}
    {e.name}
    {e.dayOff > 0 && ( отгулы: {e.dayOff} )} {e.sick > 0 && ( больн.: {e.sick} )}
  • ))}

Дисциплинарные нарушения

{disciplinary.length === 0 && (

Нет данных

)}
    {disciplinary.map((e) => (
  • {e.initials}
    {e.name}
    {e.absence > 0 && ( прогулы: {e.absence} )} {e.late > 0 && ( опозд.: {e.late} )} {e.earlyLeave > 0 && ( уходы: {e.earlyLeave} )}
  • ))}
)}
); }; const StatCard = ({ icon: Icon, label, value, subValue, color, bg, onClick }: any) => (
{subValue != null && subValue !== '' && ( {subValue} )}

{value}

{label}

); const MONTH_NAMES = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь']; const WEEKDAYS = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; const HolidayCalendar: React.FC<{ holidays: { date: string; name: string }[]; currentYear: number; onYearChange: (y: number) => void; }> = ({ holidays, currentYear, onYearChange }) => { const today = useMemo(() => new Date(), []); const [viewMonth, setViewMonth] = useState(today.getMonth()); const [viewYear, setViewYear] = useState(currentYear); const displayYear = viewYear; const holidaysByDate = useMemo(() => { const map: Record = {}; holidays.forEach((h) => { map[h.date] = h.name; }); return map; }, [holidays]); if (viewYear !== currentYear) setViewYear(currentYear); const firstDay = new Date(displayYear, viewMonth, 1); const lastDay = new Date(displayYear, viewMonth + 1, 0); const startWeekday = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1; const daysInMonth = lastDay.getDate(); const cells: (number | null)[] = []; for (let i = 0; i < startWeekday; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(d); const rows: (number | null)[][] = []; for (let i = 0; i < cells.length; i += 7) rows.push(cells.slice(i, i + 7)); const toDateStr = (day: number) => `${displayYear}-${String(viewMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const isToday = (day: number) => today.getDate() === day && today.getMonth() === viewMonth && today.getFullYear() === displayYear; const goPrev = () => { if (viewMonth === 0) { setViewMonth(11); setViewYear((y) => y - 1); onYearChange(displayYear - 1); } else setViewMonth((m) => m - 1); }; const goNext = () => { if (viewMonth === 11) { setViewMonth(0); setViewYear((y) => y + 1); onYearChange(displayYear + 1); } else setViewMonth((m) => m + 1); }; return (

Праздники РФ

{MONTH_NAMES[viewMonth]} {displayYear}
{WEEKDAYS.map((wd) => (
{wd}
))} {rows.flatMap((row, ri) => row.map((day, di) => { if (day === null) { return
; } const dateStr = toDateStr(day); const holidayName = holidaysByDate[dateStr]; const todayCell = isToday(day); return (
{day}
); }) )}

Наведите на отмеченную дату — подсказка с названием праздника

); };