import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext";
import Forbidden from "../components/Forbidden";
import BulkAttendanceModal from "../components/BulkAttendanceModal";
import ShiftFormModal from "../components/ShiftFormModal";
import AttendanceShiftTable from "../components/AttendanceShiftTable";
import useModalLock from "../hooks/useModalLock";
import useAttendanceAdmin from "../hooks/useAttendanceAdmin";
import { formatMinutes, formatHoursDecimal } from "../utils/attendanceHelpers";
import {
Button,
Card,
ConfirmDialog,
FilterBar,
LoadingState,
MonthField,
PageEnter,
Select,
StatCard,
StatusChip,
ProgressBar,
type ProgressColor,
type StatCardColor,
} from "../ui";
interface UserTotalData {
name: string;
minutes: number;
working: boolean;
vacation_hours: number;
sick_hours: number;
unpaid_hours: number;
fund: number | null;
worked_hours: number;
// Raw worked hours incl. sub-day remainders (set by computeUserTotals in
// useAttendanceAdmin); used for the Fond "used" calculation below.
worked_hours_raw?: number;
covered: number;
missing: number;
overtime: number;
// mzda-style summary fields (set by computeUserTotals in useAttendanceAdmin)
odpracovano?: number;
vcetne_svatku?: number;
prescas?: number;
svatek?: number;
worked_holiday_hours?: number;
}
/** Fond "used" total mirrors the Fond footer line below:
* real_worked + vacation + worked_holiday + free_holiday. */
function getFondUsed(data: UserTotalData): number {
const realWorked = data.worked_hours_raw ?? data.worked_hours;
return (
realWorked +
(data.vacation_hours ?? 0) +
(data.worked_holiday_hours ?? 0) +
(data.svatek ?? 0)
);
}
/** Maps the Fond delta (used − fund) to a ProgressBar color token, mirroring
* the legacy getFundBarBackground conditional: over fund → warning, exactly
* at fund → success, under fund → primary (was the accent gradient). */
function getFundBarColor(data: UserTotalData): ProgressColor {
const delta = getFondUsed(data) - (data.fund ?? 0);
if (delta > 0) return "warning";
if (delta >= 0) return "success";
return "primary";
}
const PrintIcon = (
);
const AddIcon = (
);
export default function AttendanceAdmin() {
const alert = useAlert();
const { hasPermission } = useAuth();
const {
loading,
month,
setMonth,
filterUserId,
setFilterUserId,
data,
hasData,
showBulkModal,
setShowBulkModal,
bulkSubmitting,
bulkForm,
setBulkForm,
showCreateModal,
setShowCreateModal,
createForm,
setCreateForm,
showEditModal,
setShowEditModal,
editingRecord,
editForm,
setEditForm,
deleteConfirm,
setDeleteConfirm,
projectList,
createProjectLogs,
setCreateProjectLogs,
editProjectLogs,
setEditProjectLogs,
openCreateModal,
handleCreateShiftDateChange,
handleCreateSubmit,
openBulkModal,
toggleBulkUser,
toggleAllBulkUsers,
handleBulkSubmit,
openEditModal,
handleEditSubmit,
handleDelete,
handlePrint,
} = useAttendanceAdmin({ alert });
useModalLock(showBulkModal);
useModalLock(showEditModal);
useModalLock(showCreateModal);
if (!hasPermission("attendance.manage")) return ;
// Show spinner only on initial load (no data yet), not on filter changes
const isInitialLoad =
loading &&
data.records.length === 0 &&
Object.keys(data.user_totals).length === 0;
if (isInitialLoad) {
return ;
}
const hasTotals = Object.keys(data.user_totals).length > 0;
return (
Správa docházky
{hasData && (
)}
{/* Filters */}
setMonth(val)} />
{/* User Totals (KPI cards) */}
{hasTotals && (
{Object.entries(data.user_totals).map(([uid, userData]) => {
const ut = userData as UserTotalData;
const balance = data.leave_balances[uid];
const statColor: StatCardColor = ut.working ? "success" : "default";
return (
{formatMinutes(ut.minutes)}
}
color={statColor}
footer={
{/* Leave-type badges */}
{ut.vacation_hours > 0 && (
)}
{ut.sick_hours > 0 && (
)}
{ut.svatek !== undefined && ut.svatek > 0 && (
)}
{ut.unpaid_hours > 0 && (
)}
{/* Fond usage */}
{ut.fund !== null &&
(() => {
// Fond "used" = real_worked + vacation +
// worked_holiday + free_holiday (everything the user
// actually got paid for this month).
const fondUsed = getFondUsed(ut);
const fundVal = ut.fund ?? 0;
const delta = fondUsed - fundVal;
const pct = Math.min(
100,
(fondUsed / (ut.fund || 1)) * 100,
);
return (
Fond: {formatHoursDecimal(fondUsed * 60)}h /{" "}
{formatHoursDecimal(fundVal * 60)}h
{delta > 0 && (
+{formatHoursDecimal(delta * 60)}h
)}
{delta < 0 && (
-{formatHoursDecimal(Math.abs(delta) * 60)}h
)}
);
})()}
{/* Remaining vacation */}
{balance ? (
Zbývá dovolené:{" "}
{balance.vacation_remaining.toFixed(1)}h /{" "}
{balance.vacation_total}h
) : (
—
)}
}
/>
);
})}
)}
{/* Records Table */}
setDeleteConfirm({ show: true, record })}
/>
{/* Modals */}
setShowBulkModal(false)}
form={bulkForm}
setForm={setBulkForm}
users={data.users}
onSubmit={handleBulkSubmit}
submitting={bulkSubmitting}
toggleUser={toggleBulkUser}
toggleAllUsers={toggleAllBulkUsers}
/>
setShowCreateModal(false)}
onSubmit={handleCreateSubmit}
form={createForm}
setForm={setCreateForm}
projectLogs={createProjectLogs}
setProjectLogs={setCreateProjectLogs}
projectList={projectList}
users={data.users}
onShiftDateChange={handleCreateShiftDateChange}
editingRecord={null}
/>
setShowEditModal(false)}
onSubmit={handleEditSubmit}
form={editForm}
setForm={setEditForm}
projectLogs={editProjectLogs}
setProjectLogs={setEditProjectLogs}
projectList={projectList}
users={data.users}
onShiftDateChange={handleCreateShiftDateChange}
editingRecord={
editingRecord
? {
user_name: editingRecord.user_name ?? "",
shift_date: editingRecord.shift_date,
}
: null
}
/>
setDeleteConfirm({ show: false, record: null })}
onConfirm={handleDelete}
title="Smazat záznam"
message="Opravdu chcete smazat tento záznam docházky?"
confirmText="Smazat"
confirmVariant="danger"
/>
);
}