From abbff0bfbfe719746e285a030f5d7ff83d8014ec Mon Sep 17 00:00:00 2001 From: BOHA Date: Sun, 7 Jun 2026 00:43:26 +0200 Subject: [PATCH] feat(mui): migrate Attendance History (Moje historie) onto MUI kit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/admin/pages/AttendanceHistory.tsx | 612 +++++++++++++------------- 1 file changed, 311 insertions(+), 301 deletions(-) diff --git a/src/admin/pages/AttendanceHistory.tsx b/src/admin/pages/AttendanceHistory.tsx index 2910dc9..0d1d9dc 100644 --- a/src/admin/pages/AttendanceHistory.tsx +++ b/src/admin/pages/AttendanceHistory.tsx @@ -1,9 +1,10 @@ import { useState, useMemo, useRef } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { motion } from "framer-motion"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; 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, @@ -25,7 +26,20 @@ import { normalizeDateStr, formatHoursDecimal, } from "../utils/attendanceHelpers"; -import FormField from "../components/FormField"; +import { + Button, + Card, + DataTable, + Field, + FilterBar, + MonthField, + ProgressBar, + StatusChip, + PageHeader, + EmptyState, + LoadingState, + type DataColumn, +} from "../ui"; const MONTH_NAMES = [ "Leden", @@ -42,6 +56,21 @@ const MONTH_NAMES = [ "Prosinec", ]; +const PrintIcon = ( + + + + + +); + const formatBreakRange = (record: AttendanceRecord): string => { if (record.break_start && record.break_end) { return `${formatTime(record.break_start)} - ${formatTime(record.break_end)}`; @@ -122,9 +151,7 @@ function getBusinessDaysInMonth(year: number, month: number): number { 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, @@ -145,31 +172,28 @@ const renderProjectCell = (record: AttendanceRecord) => { m = mins % 60; } return ( - - {log.project_name || `#${log.project_id}`} ({h}: - {String(m).padStart(2, "0")}h{isActive ? " ▸" : ""}) - + color={isActive ? "info" : "default"} + label={`${log.project_name || `#${log.project_id}`} (${h}:${String(m).padStart(2, "0")}h${isActive ? " ▸" : ""})`} + sx={{ fontSize: "0.7rem" }} + /> ); })} -
+ ); } if (record.project_name) { return ( - - {record.project_name} - + ); } return "—"; @@ -373,324 +397,292 @@ export default function AttendanceHistory() { }; }; + const fund = computed.monthlyFund; + const progressValue = + fund.fund > 0 ? Math.min(100, (fund.worked / fund.fund) * 100) : 0; + const progressColor = + computed.delta > 0 + ? "warning" + : computed.delta >= 0 + ? "success" + : "primary"; + const deltaColor = + computed.delta > 0 + ? "warning.main" + : computed.delta < 0 + ? "error.main" + : "success.main"; + + const columns: DataColumn[] = [ + { + key: "date", + header: "Datum", + width: "10%", + mono: true, + render: (record) => formatDate(record.shift_date), + }, + { + key: "type", + header: "Typ", + width: "11%", + render: (record) => { + const leaveType = record.leave_type || "work"; + return ( + + ); + }, + }, + { + key: "arrival", + header: "Příchod", + width: "10%", + mono: true, + render: (record) => { + const isLeave = (record.leave_type || "work") !== "work"; + return isLeave ? "—" : formatDatetime(record.arrival_time); + }, + }, + { + key: "break", + header: "Pauza", + width: "11%", + mono: true, + render: (record) => { + const isLeave = (record.leave_type || "work") !== "work"; + return isLeave ? "—" : formatBreakRange(record); + }, + }, + { + key: "departure", + header: "Odchod", + width: "10%", + mono: true, + render: (record) => { + const isLeave = (record.leave_type || "work") !== "work"; + return isLeave ? "—" : formatDatetime(record.departure_time); + }, + }, + { + key: "hours", + header: "Hodiny", + width: "9%", + mono: true, + render: (record) => { + const leaveType = record.leave_type || "work"; + const isLeave = leaveType !== "work"; + const workMinutes = isLeave + ? (Number(record.leave_hours) || 8) * 60 + : calculateWorkMinutes(record); + return workMinutes > 0 ? formatMinutes(workMinutes, true) : "—"; + }, + }, + { + key: "projects", + header: "Projekty", + width: "18%", + render: (record) => renderProjectCell(record), + }, + { + key: "notes", + header: "Poznámka", + width: "11%", + render: (record) => record.notes || "", + }, + ]; + return ( -
+ -
-

Historie docházky

-

{computed.monthName}

-
-
- {records.length > 0 && ( - - )} -
+ Tisk + + ) : undefined + } + />
{/* Filters */} -
-
- - setMonth(val)} - /> - -
-
+ + + + setMonth(val)} /> + + +
{/* Monthly Fund Card */} -
+ {isPending ? ( -
-
-
+ ) : ( - <> - {computed.monthlyFund && ( -
+ + + + + + + + + + + + Fond: {fund.worked}h / {fund.fund}h + + + {fund.business_days} prac. dnů + {fund.holiday_count > 0 && + ` + ${fund.holiday_count} svátky`} + + + + -
- - - - - - -
-
-
- - 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 && ( + -
-
-
- {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 -
- )} - + )} + {computed.vacationHours > 0 && ( + + )} + {computed.sickHours > 0 && ( + + )} + {computed.freeHolidayHours > 0 && ( + + )} + {computed.unpaidHours > 0 && ( + + )} +
+ + {computed.delta > 0 + ? `Přesčas: +${formatHoursDecimal(computed.delta * 60)}h` + : computed.delta < 0 + ? `Zbývá: ${formatHoursDecimal(-computed.delta * 60)}h` + : "Fond splněn"} + +
+ + )} -
+ {/* 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 ( - - - - - - - - - - - ); - })} - -
DatumTypPříchodPauzaOdchodHodinyProjektyPozná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 || ""} -
-
- )} - + + columns={columns} + rows={records} + rowKey={(record) => record.id} + empty={ + + } + /> )} -
+
{/* Hidden Print Content */} @@ -896,6 +888,24 @@ export default function AttendanceHistory() {
)} -
+ ); } + +/** Map an attendance leave_type to a StatusChip semantic color (mirrors the + * legacy getLeaveTypeBadgeClass colors): work→info, vacation→info(blue), + * sick→error(red), unpaid→default(gray). */ +function leaveTypeChipColor( + type: string, +): "default" | "success" | "error" | "warning" | "info" { + switch (type) { + case "vacation": + return "info"; + case "sick": + return "error"; + case "unpaid": + return "default"; + default: + return "info"; + } +}