import { useState } from "react"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import { useQuery } from "@tanstack/react-query"; import { attendanceBalancesOptions, attendanceWorkFundOptions, attendanceProjectReportOptions, type BalanceEntry, type FundUserData, } from "../lib/queries/attendance"; import { useApiMutation } from "../lib/queries/mutations"; import { Card, DataTable, Modal, ConfirmDialog, Field, TextField, Select, StatusChip, PageHeader, PageEnter, EmptyState, LoadingState, ProgressBar, type DataColumn, type ProgressColor, } from "../ui"; const API_BASE = "/api/admin"; const EditIcon = ( ); const ResetIcon = ( ); /** Maps the remaining-vacation value to a kit color token (mirrors legacy * getVacationClass: <=0 danger, <20 warning, otherwise default text color). */ const getVacationColor = (remaining: number): string | undefined => { if (remaining <= 0) return "error.main"; if (remaining < 20) return "warning.main"; return undefined; }; const renderFundDiff = (data: { overtime: number; missing: number }) => { if (data.overtime > 0) { return ( +{data.overtime}h ); } if (data.missing > 0) { return ( -{data.missing}h ); } return ( 0h ); }; const renderMonthlyStatus = ( us: FundUserData, isFulfilled: boolean, isCurrentMonth: boolean, ) => { if (us.overtime > 0) { return ; } if (us.missing > 0) { return ; } if (isFulfilled && !isCurrentMonth) { return ; } return null; }; /** Maps a user's monthly fund state to a ProgressBar color token. Mirrors the * legacy getProgressBackground gradient conditional: overtime→warning, * fulfilled→success, current month→primary (was the accent gradient), * otherwise→error (deficit). */ const getProgressColor = ( us: FundUserData, isFulfilled: boolean, isCurrentMonth: boolean, ): ProgressColor => { if (us.overtime > 0) return "warning"; if (isFulfilled) return "success"; if (isCurrentMonth) return "primary"; return "error"; }; export default function AttendanceBalances() { const alert = useAlert(); const { hasPermission } = useAuth(); const [year, setYear] = useState(new Date().getFullYear()); const { data: balancesData, isPending: balancesPending } = useQuery( attendanceBalancesOptions(year), ); const { data: fundData, isPending: fundPending } = useQuery( attendanceWorkFundOptions(year), ); const { data: projectData, isPending: projectPending } = useQuery( attendanceProjectReportOptions(year), ); const [showEditModal, setShowEditModal] = useState(false); const [editingUser, setEditingUser] = useState<{ id: string; name: string; } | null>(null); const [editForm, setEditForm] = useState({ vacation_total: 160, vacation_used: 0, sick_used: 0, }); const [resetConfirm, setResetConfirm] = useState<{ show: boolean; userId: string | null; userName: string; }>({ show: false, userId: null, userName: "" }); const editMutation = useApiMutation< { user_id: string; year: number; action_type: "edit"; vacation_total: number; vacation_used: number; sick_used: number; }, { message?: string; error?: string } >({ url: () => `${API_BASE}/attendance?action=balances`, method: () => "POST", invalidate: ["attendance", "users"], }); const resetMutation = useApiMutation< { user_id: string; year: number; action_type: "reset" }, { message?: string; error?: string } >({ url: () => `${API_BASE}/attendance?action=balances`, method: () => "POST", invalidate: ["attendance", "users"], onSuccess: (data) => { setResetConfirm({ show: false, userId: null, userName: "" }); alert.success(data?.message || "Operace dokončena"); }, }); if (!hasPermission("attendance.balances")) return ; const openEditModal = (userId: string, balance: BalanceEntry) => { setEditingUser({ id: userId, name: balance.name }); setEditForm({ vacation_total: balance.vacation_total, vacation_used: balance.vacation_used, sick_used: balance.sick_used, }); setShowEditModal(true); }; const handleEditSubmit = async () => { if (!editingUser) return; try { const result = await editMutation.mutateAsync({ user_id: editingUser.id, year, action_type: "edit", ...editForm, }); setShowEditModal(false); alert.success(result?.message || "Upraveno"); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const handleReset = async () => { if (!resetConfirm.userId) return; try { await resetMutation.mutateAsync({ user_id: resetConfirm.userId, year, action_type: "reset", }); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const years: number[] = []; const currentYear = new Date().getFullYear(); const currentMonth = new Date().getMonth() + 1; for (let y = currentYear - 5; y <= currentYear + 5; y++) { years.push(y); } const getYearFundTotals = (userId: string) => { if (!fundData?.months || Object.keys(fundData.months).length === 0) return null; let totalFund = 0; let totalWorked = 0; let totalCovered = 0; for (const monthData of Object.values(fundData.months)) { // Use prorated fund (fund_to_date) for current month, full fund for past totalFund += monthData.fund_to_date ?? monthData.fund; const us = monthData.users?.[userId]; if (us) { totalWorked += us.worked; totalCovered += us.covered; } } const missing = Math.max( 0, Math.round((totalFund - totalCovered) * 10) / 10, ); const overtime = Math.max( 0, Math.round((totalCovered - totalFund) * 10) / 10, ); return { fund: totalFund, worked: Math.round(totalWorked * 10) / 10, covered: Math.round(totalCovered * 10) / 10, missing, overtime, }; }; const balanceRows = balancesData ? Object.entries(balancesData.balances).map(([userId, balance]) => ({ userId, balance, })) : []; const columns: DataColumn<{ userId: string; balance: BalanceEntry }>[] = [ { key: "name", header: "Zaměstnanec", width: "20%", bold: true, render: ({ balance }) => balance.name, }, { key: "vacation_total", header: "Nárok (h)", width: "10%", mono: true, render: ({ balance }) => balance.vacation_total, }, { key: "vacation_used", header: "Čerpáno (h)", width: "10%", mono: true, render: ({ balance }) => balance.vacation_used.toFixed(1), }, { key: "vacation_remaining", header: "Zbývá (h)", width: "10%", mono: true, render: ({ balance }) => ( {balance.vacation_remaining.toFixed(1)} ), }, { key: "sick_used", header: "Nemoc (h)", width: "9%", mono: true, render: ({ balance }) => balance.sick_used.toFixed(1), }, { key: "fund", header: "Fond roku", width: "9%", mono: true, render: ({ userId }) => { const yf = getYearFundTotals(userId); return yf ? `${yf.fund}h` : "—"; }, }, { key: "covered", header: "Pokryto", width: "9%", mono: true, render: ({ userId }) => { const yf = getYearFundTotals(userId); return yf ? `${yf.covered}h` : "—"; }, }, { key: "diff", header: "+/−", width: "8%", mono: true, render: ({ userId }) => { const yf = getYearFundTotals(userId); return yf ? renderFundDiff(yf) : "—"; }, }, { key: "actions", header: "Akce", width: "10%", align: "right", render: ({ userId, balance }) => ( openEditModal(userId, balance)} aria-label="Upravit" title="Upravit" > {EditIcon} setResetConfirm({ show: true, userId, userName: balance.name, }) } aria-label="Resetovat" title="Resetovat" > {ResetIcon} ), }, ]; return (