import { useState, useMemo, useRef } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import { motion } from "framer-motion"; import AdminDatePicker from "../components/AdminDatePicker"; import { companySettingsOptions } from "../lib/queries/settings"; import { attendanceHistoryOptions, type AttendanceRecord, type ProjectLogEntry, } from "../lib/queries/attendance"; import { formatDate, formatDatetime, formatTime, calculateWorkMinutes, formatMinutes, getLeaveTypeName, getLeaveTypeBadgeClass, calculateWorkMinutesPrint, formatTimeOrDatetimePrint, calculateFreeHolidayHours, holidaysInMonth, normalizeDateStr, formatHoursDecimal, } from "../utils/attendanceHelpers"; import FormField from "../components/FormField"; 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 "—"; }; function getEasterSunday(year: number): string { const a = year % 19; const b = Math.floor(year / 100); const c = year % 100; const d = Math.floor(b / 4); const e = b % 4; const f = Math.floor((b + 8) / 25); const g = Math.floor((b - f + 1) / 3); const h = (19 * a + b - d - g + 15) % 30; const i = Math.floor(c / 4); const k = c % 4; const l = (32 + 2 * e + 2 * i - h - k) % 7; const m = Math.floor((a + 11 * h + 22 * l) / 451); const month = Math.floor((h + l - 7 * m + 114) / 31); const day = ((h + l - 7 * m + 114) % 31) + 1; return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; } function getCzechHolidays(year: number): string[] { const y = String(year); const holidays = [ `${y}-01-01`, `${y}-05-01`, `${y}-05-08`, `${y}-07-05`, `${y}-07-06`, `${y}-09-28`, `${y}-10-28`, `${y}-11-17`, `${y}-12-24`, `${y}-12-25`, `${y}-12-26`, ]; const easterSunday = getEasterSunday(year); const easterDate = new Date(easterSunday); const goodFriday = new Date(easterDate); goodFriday.setDate(goodFriday.getDate() - 2); const easterMonday = new Date(easterDate); easterMonday.setDate(easterMonday.getDate() + 1); const pad = (n: number) => String(n).padStart(2, "0"); holidays.push( `${goodFriday.getFullYear()}-${pad(goodFriday.getMonth() + 1)}-${pad(goodFriday.getDate())}`, ); holidays.push( `${easterMonday.getFullYear()}-${pad(easterMonday.getMonth() + 1)}-${pad(easterMonday.getDate())}`, ); holidays.sort(); return holidays; } function getBusinessDaysInMonth(year: number, month: number): number { const holidays = getCzechHolidays(year); let count = 0; const daysInMonth = new Date(year, month + 1, 0).getDate(); for (let day = 1; day <= daysInMonth; day++) { const date = new Date(year, month, day); const dow = date.getDay(); if (dow !== 0 && dow !== 6) { const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; if (!holidays.includes(dateStr)) { count++; } } } return count; } 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.max( 0, 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 { user, hasPermission } = useAuth(); const queryClient = useQueryClient(); const { data: companySettings } = useQuery(companySettingsOptions()); const companyName = companySettings?.company_name || ""; const printRef = useRef(null); const [month, setMonth] = useState(() => { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; }); const { data, isPending } = useQuery( attendanceHistoryOptions({ month, userId: user?.id }), ); const records = data ?? []; 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 unpaidHours = 0; for (const record of records) { const leaveType = record.leave_type || "work"; if (leaveType === "work") { totalMinutes += calculateWorkMinutes(record); } else { const hours = record.leave_hours != null ? Number(record.leave_hours) : 8; if (leaveType === "vacation") vacationHours += hours; else if (leaveType === "sick") sickHours += hours; else if (leaveType === "unpaid") unpaidHours += hours; // "holiday" is no longer a user-selectable leave_type — it is // auto-computed from Czech public holidays by the print/KPI code. // Old records may still exist; skip so they don't double-count. } } const yr = parseInt(yearStr, 10); const mo = parseInt(monthStr, 10) - 1; const businessDays = getBusinessDaysInMonth(yr, mo); // Fund counts Mon-Fri + holidays (holidays are paid via the fond). const holidayDates = holidaysInMonth(yr, mo + 1); const holidayCount = holidayDates.length; const fund = (businessDays + holidayCount) * 8; const worked = Math.round((totalMinutes / 60) * 100) / 100; // Covered = worked + vacation + sick (NOT unpaid — unpaid is voluntary). const leaveHours = vacationHours + sickHours; const covered = Math.round((worked + leaveHours) * 100) / 100; // fondUsed = real_worked + vacation + worked_holiday + free_holiday. // (the all-in number: everything the user got paid for this month). const realWorked = totalMinutes / 60; const freeHolidayHours = calculateFreeHolidayHours( records as unknown as Parameters[0], holidayDates, ); const workedHolidayHours = records .filter( (r) => (r.leave_type || "work") === "work" && holidayDates.includes(normalizeDateStr(r.shift_date)), ) .reduce((sum, r) => sum + calculateWorkMinutesPrint(r) / 60, 0); const fondUsed = realWorked + vacationHours + workedHolidayHours + freeHolidayHours; const delta = fondUsed - fund; const missing = Math.max(0, Math.round(-delta * 100) / 100); const overtime = Math.max(0, Math.round(delta * 100) / 100); const remaining = missing; const monthlyFund = { fund, business_days: businessDays, holiday_count: holidayCount, worked: Math.round(fondUsed * 100) / 100, covered, remaining, overtime, }; return { monthName, totalMinutes, vacationHours, sickHours, unpaidHours, monthlyFund, fondUsed, freeHolidayHours, workedHolidayHours, delta, }; }, [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 */}
{isPending ? (
) : ( <> {computed.monthlyFund && (
Fond: {computed.monthlyFund.worked}h /{" "} {computed.monthlyFund.fund}h {computed.monthlyFund.business_days} prac. dnů {computed.monthlyFund.holiday_count > 0 && ` + ${computed.monthlyFund.holiday_count} svátky`}
0 ? (computed.monthlyFund.worked / computed.monthlyFund.fund) * 100 : 0)}%`, background: computed.delta > 0 ? "linear-gradient(135deg, var(--warning), #d97706)" : computed.delta >= 0 ? "linear-gradient(135deg, var(--success), #059669)" : "var(--gradient)", }} />
{computed.totalMinutes > 0 && ( Práce: {formatHoursDecimal(computed.totalMinutes)}h )} {computed.vacationHours > 0 && ( Dov: {computed.vacationHours}h )} {computed.sickHours > 0 && ( Nem: {computed.sickHours}h )} {computed.freeHolidayHours > 0 && ( Sv: {Math.round(computed.freeHolidayHours)}h )} {computed.unpaidHours > 0 && ( Nep: {computed.unpaidHours}h )}
0 ? "text-warning fw-600" : computed.delta < 0 ? "text-danger fw-600" : "text-success fw-600" } > {computed.delta > 0 ? `Přesčas: +${formatHoursDecimal(computed.delta * 60)}h` : computed.delta < 0 ? `Zbývá: ${formatHoursDecimal(-computed.delta * 60)}h` : "Fond splněn"}
)} {!computed.monthlyFund && (
Fond měsíce není k dispozici
)} )}
{/* Records Table */}
{isPending ? (
) : ( <> {records.length === 0 && (

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

)} {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.vacationHours > 0 && ( <> Dovolená: {computed.vacationHours}h {" "} )} {computed.sickHours > 0 && ( <> Nemoc: {computed.sickHours}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)}
)}
); }