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 && (
-
-
-
-
- | Datum |
- Typ |
- Příchod |
- Pauza |
- Odchod |
- Hodiny |
- Projekty |
- Poznámka |
-
-
-
- {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 (
-
- |
- {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";
+ }
+}