diff --git a/src/admin/pages/Attendance.tsx b/src/admin/pages/Attendance.tsx index 0194937..93c0102 100644 --- a/src/admin/pages/Attendance.tsx +++ b/src/admin/pages/Attendance.tsx @@ -1,23 +1,40 @@ import { useState, useEffect, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; +import { Link as RouterLink } from "react-router-dom"; +import { motion } from "framer-motion"; +import { parseISO, isValid } from "date-fns"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; -import { Link } from "react-router-dom"; -import { motion } from "framer-motion"; -import AdminDatePicker from "../components/AdminDatePicker"; -import ConfirmModal from "../components/ConfirmModal"; -import FormModal from "../components/FormModal"; import { formatTime, calculateWorkMinutes, formatMinutes, } from "../utils/attendanceHelpers"; -import FormField from "../components/FormField"; import Forbidden from "../components/Forbidden"; import apiFetch from "../utils/api"; import { jsonQuery } from "../lib/apiAdapter"; import { useApiMutation } from "../lib/queries/mutations"; import { czechPlural, todayLocalStr } from "../utils/formatters"; +import { + Button, + Card, + DataTable, + DateField, + Field, + Modal, + ConfirmDialog, + Select, + StatCard, + StatusChip, + TextField, + ProgressBar, + EmptyState, + LoadingState, + type DataColumn, + type ProgressColor, +} from "../ui"; const API_BASE = "/api/admin"; @@ -78,12 +95,162 @@ interface AttendanceData { active_project_id: number | null; } -function getFundBarBackground(fund: MonthlyFund) { - if (fund.overtime > 0) - return "linear-gradient(135deg, var(--warning), #d97706)"; - if (fund.covered >= fund.fund) - return "linear-gradient(135deg, var(--success), #059669)"; - return "var(--gradient)"; +/** Maps the monthly-fund state to a ProgressBar color token. Mirrors the legacy + * getFundBarBackground gradient conditional: overtime→warning, covered≥fund→ + * success, otherwise→primary (was the accent gradient). */ +function getFundBarColor(fund: MonthlyFund): ProgressColor { + if (fund.overtime > 0) return "warning"; + if (fund.covered >= fund.fund) return "success"; + return "primary"; +} + +const CalendarIcon = ( + + + + + + +); + +const VacationIcon = ( + + + +); + +const SickIcon = ( + + + +); + +const RequestsIcon = ( + + + + +); + +const HistoryIcon = ( + + + + +); + +const ManageIcon = ( + + + + + + +); + +const BalancesIcon = ( + + + +); + +const ChevronIcon = ( + + + +); + +/** Sidebar navigation row: icon + label on the left, chevron on the right. */ +function QuickLink({ + to, + icon, + label, +}: { + to: string; + icon: React.ReactNode; + label: string; +}) { + return ( + + ); } export default function Attendance() { @@ -344,11 +511,7 @@ export default function Attendance() { }; if (statusQuery.isPending) { - return ( -
-
-
- ); + return ; } if (!statusQuery.data) { return null; @@ -368,118 +531,312 @@ export default function Attendance() { const vacationDaysRemaining = Math.floor(leaveBalance.vacation_remaining / 8); const vacationHoursRemaining = leaveBalance.vacation_remaining % 8; + const businessDays = calculateBusinessDays( + leaveForm.date_from, + leaveForm.date_to, + ); + + const completedColumns: DataColumn[] = [ + { + key: "arrival", + header: "Příchod", + mono: true, + render: (shift) => formatTime(shift.arrival_time), + }, + { + key: "break", + header: "Pauza", + mono: true, + render: (shift) => + shift.break_start && shift.break_end + ? `${formatTime(shift.break_start)} - ${formatTime(shift.break_end)}` + : "—", + }, + { + key: "departure", + header: "Odchod", + mono: true, + render: (shift) => formatTime(shift.departure_time), + }, + { + key: "worked", + header: "Odpracováno", + mono: true, + render: (shift) => + formatMinutes( + calculateWorkMinutes({ + ...shift, + notes: shift.notes ?? undefined, + }), + true, + ), + }, + ...(projects.length > 0 + ? [ + { + key: "projects", + header: "Projekty", + render: (shift: ShiftRecord) => { + const shiftLogs = shift.project_logs || []; + if (shiftLogs.length === 0) return "—"; + return ( + + {shiftLogs.map((log, i) => { + if (!log.started_at) return null; + const mins = log.ended_at + ? Math.floor( + (new Date(log.ended_at).getTime() - + new Date(log.started_at).getTime()) / + 60000, + ) + : 0; + const h = Math.floor(mins / 60); + const mm = mins % 60; + return ( + + {log.project_name || `#${log.project_id}`} ({h}: + {String(mm).padStart(2, "0")}h) + + ); + })} + + ); + }, + } as DataColumn, + ] + : []), + ]; + return ( -
+ -
-

Docházka

-

+ + Docházka + {new Date().toLocaleDateString("cs-CZ", { weekday: "long", day: "numeric", month: "long", year: "numeric", })} -

-
+ +
-
+ {/* Left Column - Clock In/Out */} -
-
-
- {isOngoingShift ? ( - <> - - Pracuji - - ) : ( - <> - - Nepracuji - - )} -
-
+ + + + `0 0 0 4px ${theme.palette.success.main}33` + : "none", + }} + /> + + {isOngoingShift ? "Pracuji" : "Nepracuji"} + + + {new Date().toLocaleTimeString("cs-CZ", { hour: "2-digit", minute: "2-digit", })} -
-
+ + {isOngoingShift ? ( <> -
-
-
- Příchod - - {formatTime(ongoingShift.arrival_time)} - -
-
- Pauza - - {ongoingShift.break_start - ? `${formatTime(ongoingShift.break_start)} - ${formatTime(ongoingShift.break_end)}` - : "—"} - -
-
- Odchod - -
-
-
+ + + + Příchod + + + {formatTime(ongoingShift.arrival_time)} + + + + + Pauza + + + {ongoingShift.break_start + ? `${formatTime(ongoingShift.break_start)} - ${formatTime(ongoingShift.break_end)}` + : "—"} + + + + + Odchod + + + — + + + {projects.length > 0 && ( -
-
- Projekt - {activeProjectId ? ( - - {projects.find( - (p) => String(p.id) === String(activeProjectId), - ) - ? `${projects.find((p) => String(p.id) === String(activeProjectId))!.project_number} – ${projects.find((p) => String(p.id) === String(activeProjectId))!.name}` - : `Projekt #${activeProjectId}`} - - ) : ( - Žádný - )} -
- + + Projekt + + {activeProjectId ? ( + String(p.id) === String(activeProjectId), + ) + ? `${projects.find((p) => String(p.id) === String(activeProjectId))!.project_number} – ${projects.find((p) => String(p.id) === String(activeProjectId))!.name}` + : `Projekt #${activeProjectId}` + } + sx={{ + height: "auto", + "& .MuiChip-label": { + whiteSpace: "normal", + py: 0.25, + }, + }} + /> + ) : ( + + Žádný + + )} + +