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 && (
-
-
-
-
- | Zaměstnanec |
- Nárok (h) |
- Čerpáno (h) |
- Zbývá (h) |
- Nemoc (h) |
- Fond roku |
- Pokryto |
- +/− |
- Akce |
-
-
-
- {Object.entries(balancesData.balances).map(
- ([userId, balance]) => {
- const yf = getYearFundTotals(userId);
- return (
-
- | {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 (
+
+
+
+ }
+ />
+
+
+ {balancesPending ? (
+
+ ) : balanceRows.length === 0 ? (
+
+ ) : (
+
+ columns={columns}
+ rows={balanceRows}
+ rowKey={(row) => row.userId}
+ />
+ )}
+
{/* Monthly Fund Overview */}
{!fundPending &&
fundData?.months &&
Object.keys(fundData.months).length > 0 && (
-
-
+
+
Měsíční přehled fondu {year}
-
-
+
+
{Object.entries(fundData.months).map(([monthKey, monthData]) => {
const isCurrentMonth =
year === currentYear && parseInt(monthKey) === currentMonth;
return (
-
+ `0 0 0 1px ${theme.palette.primary.main}`,
}
- : {}
+ : undefined
}
>
-
-
+
-
- {monthData.month_name}
- {isCurrentMonth && (
-
- aktuální
-
- )}
-
-
- {monthData.fund_to_date ?? monthData.fund}h (
- {Math.round(
- (monthData.fund_to_date ?? monthData.fund) / 8,
- )}{" "}
- dnů)
-
-
-
+ )}
+
+
- {fundData?.users &&
- fundData.users.map((user) => {
- const us = monthData.users?.[String(user.id)];
- if (!us) return null;
- const effectiveFund =
- monthData.fund_to_date ?? monthData.fund;
- const pct =
- effectiveFund > 0
- ? Math.min(
- 100,
- (us.covered / effectiveFund) * 100,
- )
- : 0;
- const isFulfilled = us.covered >= effectiveFund;
- return (
-
-
+
+
+ {fundData?.users &&
+ fundData.users.map((user) => {
+ const us = monthData.users?.[String(user.id)];
+ if (!us) return null;
+ const effectiveFund =
+ monthData.fund_to_date ?? monthData.fund;
+ const pct =
+ effectiveFund > 0
+ ? Math.min(
+ 100,
+ (us.covered / effectiveFund) * 100,
+ )
+ : 0;
+ const isFulfilled = us.covered >= effectiveFund;
+ return (
+
+
+
+ {us.name}
+
+
-
- {us.name}
-
-
-
- {us.worked}h
-
- {renderMonthlyStatus(
- us,
- isFulfilled,
- isCurrentMonth,
- )}
-
-
-
-
- );
- })}
-
-
-
+ {us.worked}h
+
+ {renderMonthlyStatus(
+ us,
+ isFulfilled,
+ isCurrentMonth,
+ )}
+
+
+
+
+ );
+ })}
+
+
);
})}
-
-
+
+
)}
- {fundPending && (
-
- )}
+ {fundPending && }
{/* Monthly Project Overview */}
{!projectPending &&
projectData?.months &&
Object.keys(projectData.months).length > 0 && (
-
-
+
+
Měsíční přehled projektů {year}
-
-
+
+
{Object.entries(projectData.months).map(
([monthKey, monthInfo]) => {
const isCurrentMonth =
@@ -581,253 +594,216 @@ export default function AttendanceBalances() {
);
if (monthInfo.projects.length === 0) return null;
return (
-
+ `0 0 0 1px ${theme.palette.primary.main}`,
}
- : {}
+ : undefined
}
>
-
-
+
-
- {monthInfo.month_name}
- {isCurrentMonth && (
-
+ )}
+
+
+ {totalHours.toFixed(1)}h
+
+
+
+ {monthInfo.projects.map((proj) => (
+
+
+
+ {proj.project_id
+ ? proj.project_number
+ : "Bez projektu"}
+
+
+ {proj.hours.toFixed(1)}h
+
+
+ {proj.project_id && proj.project_name && (
+
- aktuální
-
+ {proj.project_name}
+
)}
-
-
- {totalHours.toFixed(1)}h
-
-
-
- {monthInfo.projects.map((proj) => (
-
-
-
- {proj.project_id
- ? proj.project_number
- : "Bez projektu"}
-
-
- {proj.hours.toFixed(1)}h
-
-
- {proj.project_id && proj.project_name && (
-
- {proj.project_name}
-
- )}
-
- {proj.users.map((u) => {
- const pct =
- proj.hours > 0
- ? Math.min(
- 100,
- (u.hours / proj.hours) * 100,
- )
- : 0;
- return (
-
-
+ {proj.users.map((u) => {
+ const pct =
+ proj.hours > 0
+ ? Math.min(
+ 100,
+ (u.hours / proj.hours) * 100,
+ )
+ : 0;
+ return (
+
+
+
-
- {u.user_name}
-
-
- {u.hours.toFixed(1)}h
-
-
-
-
- );
- })}
-
-
- ))}
-
-
-
+ {u.hours.toFixed(1)}h
+
+
+
+
+ );
+ })}
+
+
+ ))}
+
+
);
},
)}
-
-
+
+
)}
- {projectPending && (
-
- )}
+ {projectPending && }
{/* Edit Modal */}
- setShowEditModal(false)}
onSubmit={handleEditSubmit}
title="Upravit dovolenou"
+ subtitle={editingUser?.name}
loading={editMutation.isPending}
>
- {editingUser && (
- <>
-
- {editingUser.name}
-
-
-
-
- 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}
/>
-
+
);
}