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 { useQuery } from "@tanstack/react-query";
import {
attendanceBalancesOptions,
attendanceWorkFundOptions,
attendanceProjectReportOptions,
type BalanceEntry,
type FundUserData,
} from "../lib/queries/attendance";
import { useApiMutation } from "../lib/queries/mutations";
import {
Card,
DataTable,
Modal,
ConfirmDialog,
Field,
TextField,
Select,
StatusChip,
PageHeader,
PageEnter,
EmptyState,
LoadingState,
ProgressBar,
type DataColumn,
type ProgressColor,
} from "../ui";
const API_BASE = "/api/admin";
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
);
}
if (data.missing > 0) {
return (
-{data.missing}h
);
}
return (
0h
);
};
const renderMonthlyStatus = (
us: FundUserData,
isFulfilled: boolean,
isCurrentMonth: boolean,
) => {
if (us.overtime > 0) {
return ;
}
if (us.missing > 0) {
return ;
}
if (isFulfilled && !isCurrentMonth) {
return ;
}
return null;
};
/** 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,
): ProgressColor => {
if (us.overtime > 0) return "warning";
if (isFulfilled) return "success";
if (isCurrentMonth) return "primary";
return "error";
};
export default function AttendanceBalances() {
const alert = useAlert();
const { hasPermission } = useAuth();
const [year, setYear] = useState(new Date().getFullYear());
const { data: balancesData, isPending: balancesPending } = useQuery(
attendanceBalancesOptions(year),
);
const { data: fundData, isPending: fundPending } = useQuery(
attendanceWorkFundOptions(year),
);
const { data: projectData, isPending: projectPending } = useQuery(
attendanceProjectReportOptions(year),
);
const [showEditModal, setShowEditModal] = useState(false);
const [editingUser, setEditingUser] = useState<{
id: string;
name: string;
} | null>(null);
const [editForm, setEditForm] = useState({
vacation_total: 160,
vacation_used: 0,
sick_used: 0,
});
const [resetConfirm, setResetConfirm] = useState<{
show: boolean;
userId: string | null;
userName: string;
}>({ show: false, userId: null, userName: "" });
const editMutation = useApiMutation<
{
user_id: string;
year: number;
action_type: "edit";
vacation_total: number;
vacation_used: number;
sick_used: number;
},
{ message?: string; error?: string }
>({
url: () => `${API_BASE}/attendance?action=balances`,
method: () => "POST",
invalidate: ["attendance", "users"],
});
const resetMutation = useApiMutation<
{ user_id: string; year: number; action_type: "reset" },
{ message?: string; error?: string }
>({
url: () => `${API_BASE}/attendance?action=balances`,
method: () => "POST",
invalidate: ["attendance", "users"],
onSuccess: (data) => {
setResetConfirm({ show: false, userId: null, userName: "" });
alert.success(data?.message || "Operace dokončena");
},
});
if (!hasPermission("attendance.balances")) return ;
const openEditModal = (userId: string, balance: BalanceEntry) => {
setEditingUser({ id: userId, name: balance.name });
setEditForm({
vacation_total: balance.vacation_total,
vacation_used: balance.vacation_used,
sick_used: balance.sick_used,
});
setShowEditModal(true);
};
const handleEditSubmit = async () => {
if (!editingUser) return;
try {
const result = await editMutation.mutateAsync({
user_id: editingUser.id,
year,
action_type: "edit",
...editForm,
});
setShowEditModal(false);
alert.success(result?.message || "Upraveno");
} catch (e) {
alert.error(e instanceof Error ? e.message : "Chyba připojení");
}
};
const handleReset = async () => {
if (!resetConfirm.userId) return;
try {
await resetMutation.mutateAsync({
user_id: resetConfirm.userId,
year,
action_type: "reset",
});
} catch (e) {
alert.error(e instanceof Error ? e.message : "Chyba připojení");
}
};
const years: number[] = [];
const currentYear = new Date().getFullYear();
const currentMonth = new Date().getMonth() + 1;
for (let y = currentYear - 5; y <= currentYear + 5; y++) {
years.push(y);
}
const getYearFundTotals = (userId: string) => {
if (!fundData?.months || Object.keys(fundData.months).length === 0)
return null;
let totalFund = 0;
let totalWorked = 0;
let totalCovered = 0;
for (const monthData of Object.values(fundData.months)) {
// Use prorated fund (fund_to_date) for current month, full fund for past
totalFund += monthData.fund_to_date ?? monthData.fund;
const us = monthData.users?.[userId];
if (us) {
totalWorked += us.worked;
totalCovered += us.covered;
}
}
const missing = Math.max(
0,
Math.round((totalFund - totalCovered) * 10) / 10,
);
const overtime = Math.max(
0,
Math.round((totalCovered - totalFund) * 10) / 10,
);
return {
fund: totalFund,
worked: Math.round(totalWorked * 10) / 10,
covered: Math.round(totalCovered * 10) / 10,
missing,
overtime,
};
};
const balanceRows = balancesData
? Object.entries(balancesData.balances).map(([userId, balance]) => ({
userId,
balance,
}))
: [];
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 (
);
}