diff --git a/src/admin/pages/AttendanceBalances.tsx b/src/admin/pages/AttendanceBalances.tsx index b1d64a2..9555f2e 100644 --- a/src/admin/pages/AttendanceBalances.tsx +++ b/src/admin/pages/AttendanceBalances.tsx @@ -1,43 +1,107 @@ 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 { motion } from "framer-motion"; -import ConfirmModal from "../components/ConfirmModal"; -import FormModal from "../components/FormModal"; -import FormField from "../components/FormField"; import { useQuery } from "@tanstack/react-query"; import { attendanceBalancesOptions, attendanceWorkFundOptions, attendanceProjectReportOptions, type BalanceEntry, - type BalancesData, - type FundData, type FundUserData, - type MonthFundData, - type ProjectData, - type UserShort, } from "../lib/queries/attendance"; import { useApiMutation } from "../lib/queries/mutations"; +import { + Card, + DataTable, + Modal, + ConfirmDialog, + Field, + TextField, + Select, + StatusChip, + PageHeader, + EmptyState, + LoadingState, + ProgressBar, + type DataColumn, + type ProgressColor, +} from "../ui"; const API_BASE = "/api/admin"; -const getVacationClass = (remaining: number): string => { - if (remaining <= 0) return "text-danger"; - if (remaining < 20) return "text-warning"; - return ""; +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; + return ( + + +{data.overtime}h + + ); } if (data.missing > 0) { - return -{data.missing}h; + return ( + + -{data.missing}h + + ); } - return 0h; + return ( + + 0h + + ); }; const renderMonthlyStatus = ( @@ -46,39 +110,30 @@ const renderMonthlyStatus = ( isCurrentMonth: boolean, ) => { if (us.overtime > 0) { - return ( - - +{us.overtime}h - - ); + return ; } if (us.missing > 0) { - return ( - - -{us.missing}h - - ); + return ; } if (isFulfilled && !isCurrentMonth) { - return ( - - OK - - ); + return ; } return null; }; -const getProgressBackground = ( +/** 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, -): string => { - if (us.overtime > 0) - return "linear-gradient(135deg, var(--warning), #d97706)"; - if (isFulfilled) return "linear-gradient(135deg, var(--success), #059669)"; - if (isCurrentMonth) return "var(--gradient)"; - return "var(--danger)"; +): ProgressColor => { + if (us.overtime > 0) return "warning"; + if (isFulfilled) return "success"; + if (isCurrentMonth) return "primary"; + return "error"; }; export default function AttendanceBalances() { @@ -221,356 +276,314 @@ export default function AttendanceBalances() { }; }; - return ( -
- -
-

Správa bilancí

-
-
- -
-
+ const balanceRows = balancesData + ? Object.entries(balancesData.balances).map(([userId, balance]) => ({ + userId, + balance, + })) + : []; - -
- {balancesPending ? ( -
-
-
- ) : ( - <> - {balancesData && - Object.keys(balancesData.balances).length === 0 && ( -
-

Žádní uživatelé k zobrazení.

-
- )} - {balancesData && - Object.keys(balancesData.balances).length > 0 && ( -
- - - - - - - - - - - - - - - - {Object.entries(balancesData.balances).map( - ([userId, balance]) => { - const yf = getYearFundTotals(userId); - return ( - - - - - - - - - - - - ); - }, - )} - -
ZaměstnanecNárok (h)Čerpáno (h)Zbývá (h)Nemoc (h)Fond rokuPokryto+/−Akce
{balance.name} - {balance.vacation_total} - - {balance.vacation_used.toFixed(1)} - - - {balance.vacation_remaining.toFixed(1)} - - - {balance.sick_used.toFixed(1)} - - {yf ? `${yf.fund}h` : "—"} - - {yf ? `${yf.covered}h` : "—"} - - {yf ? renderFundDiff(yf) : "—"} - -
- - -
-
-
- )} - - )} -
- + 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 ( + + + - setEditForm({ - ...editForm, - vacation_total: parseFloat(e.target.value), - }) - } - min="0" - max="500" - step="1" - className="admin-form-input" - /> - + + + setEditForm({ + ...editForm, + vacation_total: parseFloat(e.target.value), + }) + } + slotProps={{ htmlInput: { min: 0, max: 500, step: 1 } }} + /> + - - - setEditForm({ - ...editForm, - vacation_used: parseFloat(e.target.value), - }) - } - min="0" - max="500" - step="0.5" - className="admin-form-input" - /> - + + + setEditForm({ + ...editForm, + vacation_used: parseFloat(e.target.value), + }) + } + slotProps={{ htmlInput: { min: 0, max: 500, step: 0.5 } }} + /> + - - - setEditForm({ - ...editForm, - sick_used: parseFloat(e.target.value), - }) - } - min="0" - max="500" - step="0.5" - className="admin-form-input" - /> - -
- - )} - + + + setEditForm({ + ...editForm, + sick_used: parseFloat(e.target.value), + }) + } + slotProps={{ htmlInput: { min: 0, max: 500, step: 0.5 } }} + /> + + {/* Reset Confirmation */} - setResetConfirm({ show: false, userId: null, userName: "" }) @@ -839,6 +815,6 @@ export default function AttendanceBalances() { confirmVariant="danger" loading={resetMutation.isPending} /> -
+ ); }