import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import { motion } from "framer-motion"; import AdminDatePicker from "../components/AdminDatePicker"; import { formatDate, formatDatetime, formatTime, calculateWorkMinutes, formatMinutes, getLeaveTypeName, getLeaveTypeBadgeClass, calculateWorkMinutesPrint, formatTimeOrDatetimePrint, } from "../utils/attendanceHelpers"; import FormField from "../components/FormField"; import apiFetch from "../utils/api"; const API_BASE = "/api/admin"; interface ProjectLog { id?: number; project_id?: number; project_name?: string; started_at?: string; ended_at?: string | null; hours?: string | number | null; minutes?: string | number | null; } interface AttendanceRecord { id: number; shift_date: string; leave_type?: string; leave_hours?: number; arrival_time?: string | null; departure_time?: string | null; break_start?: string | null; break_end?: string | null; notes?: string; project_name?: string; project_logs?: ProjectLog[]; } const MONTH_NAMES = [ "Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec", ]; const formatBreakRange = (record: AttendanceRecord): string => { if (record.break_start && record.break_end) { return `${formatTime(record.break_start)} - ${formatTime(record.break_end)}`; } if (record.break_start) { return `${formatTime(record.break_start)} - ?`; } return "—"; }; const renderProjectCell = (record: AttendanceRecord) => { if (record.project_logs && record.project_logs.length > 0) { return (
{record.project_logs.map((log, i) => { let h: number, m: number, isActive = false; if (log.hours !== null && log.hours !== undefined) { h = parseInt(String(log.hours)) || 0; m = parseInt(String(log.minutes)) || 0; } else { isActive = !log.ended_at; const end = log.ended_at ? new Date(log.ended_at) : new Date(); const mins = Math.floor( (end.getTime() - new Date(log.started_at!).getTime()) / 60000, ); h = Math.floor(mins / 60); m = mins % 60; } return ( {log.project_name || `#${log.project_id}`} ({h}: {String(m).padStart(2, "0")}h{isActive ? " ▸" : ""}) ); })}
); } if (record.project_name) { return ( {record.project_name} ); } return "—"; }; export default function AttendanceHistory() { const alert = useAlert(); const { user, hasPermission } = useAuth(); const [loading, setLoading] = useState(true); const [companyName, setCompanyName] = useState(""); const printRef = useRef(null); const [month, setMonth] = useState(() => { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; }); const [records, setRecords] = useState([]); const fetchData = useCallback(async () => { setLoading(true); try { const [yearStr, monthStr] = month.split("-"); const response = await apiFetch( `${API_BASE}/attendance?year=${yearStr}&month=${monthStr}&limit=1000&user_id=${user?.id || ""}`, ); if (response.status === 401) return; const result = await response.json(); if (result.success) { setRecords(result.data); } } catch { alert.error("Nepodařilo se načíst data"); } finally { setLoading(false); } }, [month, alert, user?.id]); useEffect(() => { fetchData(); }, [fetchData]); useEffect(() => { apiFetch(`${API_BASE}/company-settings`) .then((r) => r.json()) .then((d) => { if (d.success) setCompanyName(d.data.company_name || ""); }) .catch(() => {}); }, []); const computed = useMemo(() => { const [yearStr, monthStr] = month.split("-"); const monthIndex = parseInt(monthStr, 10) - 1; const monthName = `${MONTH_NAMES[monthIndex]} ${yearStr}`; let totalMinutes = 0; let vacationHours = 0; let sickHours = 0; let holidayHours = 0; let unpaidHours = 0; for (const record of records) { const leaveType = record.leave_type || "work"; if (leaveType === "work") { totalMinutes += calculateWorkMinutes(record); } else { const hours = Number(record.leave_hours) || 8; if (leaveType === "vacation") vacationHours += hours; else if (leaveType === "sick") sickHours += hours; else if (leaveType === "holiday") holidayHours += hours; else if (leaveType === "unpaid") unpaidHours += hours; } } // Exclude holidays from business days (matching PHP CzechHolidays logic) const yr = parseInt(yearStr, 10); const mo = parseInt(monthStr, 10) - 1; const holidayDays = records.filter( (r) => (r.leave_type || "work") === "holiday", ).length; let businessDays = 0; const cur = new Date(yr, mo, 1); while (cur.getMonth() === mo) { const dow = cur.getDay(); if (dow !== 0 && dow !== 6) businessDays++; cur.setDate(cur.getDate() + 1); } // Subtract holidays from business days (holidays are non-working days, not part of the fund) businessDays = Math.max(0, businessDays - holidayDays); const fund = businessDays * 8; const worked = Math.round((totalMinutes / 60) * 100) / 100; // Covered = worked + vacation + sick (NOT holiday/unpaid — holiday is excluded from fund, unpaid is voluntary) const leaveHours = vacationHours + sickHours; const covered = Math.round((worked + leaveHours) * 100) / 100; const remaining = Math.max(0, Math.round((fund - covered) * 100) / 100); const overtime = Math.max(0, Math.round((covered - fund) * 100) / 100); const monthlyFund = { fund, business_days: businessDays, worked, covered, remaining, overtime, }; return { monthName, totalMinutes, vacationHours, sickHours, holidayHours, unpaidHours, monthlyFund, }; }, [records, month]); if (!hasPermission("attendance.history")) return ; const handlePrint = () => { if (!printRef.current) return; const content = printRef.current.innerHTML; const printWindow = window.open("", "_blank"); if (!printWindow) return; printWindow.document.write(` Docházka - ${computed.monthName} ${content} `); printWindow.document.close(); printWindow.onload = () => { printWindow.print(); }; }; return (

Historie docházky

{computed.monthName}

{records.length > 0 && ( )}
{/* Filters */}
setMonth(val)} />
{/* Monthly Fund Card */}
{loading && (
)} {!loading && computed.monthlyFund && (
Fond: {computed.monthlyFund.worked}h /{" "} {computed.monthlyFund.fund}h {computed.monthlyFund.business_days} prac. dnů
0 ? (computed.monthlyFund.covered / computed.monthlyFund.fund) * 100 : 0)}%`, background: computed.monthlyFund.covered >= computed.monthlyFund.fund ? "linear-gradient(135deg, var(--success), #059669)" : "var(--gradient)", }} />
{"Pokryto: "} {computed.monthlyFund.covered}h (práce{" "} {computed.monthlyFund.worked}h {computed.vacationHours > 0 && ` + dovolená ${computed.vacationHours}h`} {computed.sickHours > 0 && ` + nemoc ${computed.sickHours}h`} {computed.holidayHours > 0 && ` + svátek ${computed.holidayHours}h`} {computed.unpaidHours > 0 && ` + neplacené ${computed.unpaidHours}h`} ) {computed.monthlyFund.overtime > 0 ? ( Přesčas: +{computed.monthlyFund.overtime}h ) : ( Zbývá: {computed.monthlyFund.remaining}h )}
)} {!loading && !computed.monthlyFund && (
Fond měsíce není k dispozici
)}
{/* Records Table */}
{loading && (
{[0, 1, 2, 3, 4].map((i) => (
))}
)} {!loading && records.length === 0 && (

Za tento měsíc nejsou žádné záznamy.

)} {!loading && records.length > 0 && (
{records.map((record) => { const leaveType = record.leave_type || "work"; const isLeave = leaveType !== "work"; const workMinutes = isLeave ? (Number(record.leave_hours) || 8) * 60 : calculateWorkMinutes(record); return ( ); })}
Datum Typ Příchod Pauza Odchod Hodiny Projekty Poznámka
{formatDate(record.shift_date)} {getLeaveTypeName(leaveType)} {isLeave ? "—" : formatDatetime(record.arrival_time)} {isLeave ? "—" : formatBreakRange(record)} {isLeave ? "—" : formatDatetime(record.departure_time)} {workMinutes > 0 ? formatMinutes(workMinutes, true) : "—"} {renderProjectCell(record)} {record.notes || ""}
)}
{/* Hidden Print Content */} {records.length > 0 && (

EVIDENCE DOCHÁZKY

{companyName}
{computed.monthName}
Zaměstnanec: {user?.fullName || ""}
Vygenerováno: {new Date().toLocaleString("cs-CZ")}

{user?.fullName || ""}

Odpracováno:{" "} {formatMinutes(computed.totalMinutes, true)}
{(computed.vacationHours > 0 || computed.sickHours > 0 || computed.holidayHours > 0) && (
{computed.vacationHours > 0 && ( <> Dovolená: {computed.vacationHours}h {" "} )} {computed.sickHours > 0 && ( <> Nemoc: {computed.sickHours}h {" "} )} {computed.holidayHours > 0 && ( <> Svátek: {computed.holidayHours}h {" "} )}
)} {[...records] .sort((a, b) => a.shift_date.localeCompare(b.shift_date), ) .map((record) => { const leaveType = record.leave_type || "work"; const isLeave = leaveType !== "work"; const workMinutes = calculateWorkMinutesPrint(record); const hours = Math.floor(workMinutes / 60); const mins = workMinutes % 60; return ( ); })}
Datum Typ Příchod Pauza Odchod Hodiny Projekty Poznámka
{formatDate(record.shift_date)} {getLeaveTypeName(leaveType)} {isLeave ? "—" : formatTimeOrDatetimePrint( record.arrival_time, record.shift_date, )} {isLeave || !record.break_start || !record.break_end ? "—" : `${formatTimeOrDatetimePrint(record.break_start, record.shift_date)} - ${formatTimeOrDatetimePrint(record.break_end, record.shift_date)}`} {isLeave ? "—" : formatTimeOrDatetimePrint( record.departure_time, record.shift_date, )} {workMinutes > 0 ? `${hours}:${String(mins).padStart(2, "0")}` : "—"} {record.project_logs && record.project_logs.length > 0 ? record.project_logs.map((log, i) => { let h: number, m: number; if ( log.hours !== null && log.hours !== undefined ) { h = parseInt(String(log.hours)) || 0; m = parseInt(String(log.minutes)) || 0; } else if ( log.started_at && log.ended_at ) { const mins2 = Math.max( 0, Math.floor( (new Date( log.ended_at, ).getTime() - new Date( log.started_at, ).getTime()) / 60000, ), ); h = Math.floor(mins2 / 60); m = mins2 % 60; } else { h = 0; m = 0; } return (
{log.project_name || `#${log.project_id}`}{" "} ({h}:{String(m).padStart(2, "0")}h)
); }) : record.project_name || "—"}
{record.notes || ""}
Odpracováno: {formatMinutes(computed.totalMinutes, true)}
)}
); }