import { Alert, Badge, Button, Card, Group, Modal, NumberInput, Stack, Table, Tabs, Text, TextInput, Tooltip, } from "@mantine/core"; import { IconAlertTriangle, IconPlus, IconSeeding } from "@tabler/icons-react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Can } from "@/auth/Can"; import { HR_PERMS } from "@/auth/permissions"; import { apiErrorMessage } from "@/auth/http"; import { PageHeader } from "@/shared/components/PageHeader"; import { BilingualTextInput } from "@/shared/components/BilingualTextInput"; import { localized } from "@/shared/lib/localizedName"; import { createWorkSchedule, listOvertimeRates, listWorkSchedules, seedOvertimeRates, updateOvertimeRate, } from "./api"; import { formatMinutes } from "./components/AttendanceStatusBadge"; const DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; export function WorkSchedulesPage() { const queryClient = useQueryClient(); const { i18n } = useTranslation(); const [adding, setAdding] = useState(false); const schedules = useQuery({ queryKey: ["work-schedules"], queryFn: listWorkSchedules, }); const rates = useQuery({ queryKey: ["overtime-rates"], queryFn: listOvertimeRates }); const seed = useMutation({ mutationFn: seedOvertimeRates, onSuccess: () => queryClient.invalidateQueries({ queryKey: ["overtime-rates"] }), }); const updateRate = useMutation({ mutationFn: (payload: { id: string; multiplier: string }) => updateOvertimeRate(payload.id, { multiplier: payload.multiplier }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["overtime-rates"] }), }); return ( <> Shift patterns Overtime premiums Pattern Hours Break Grace Days Full day at {(schedules.data ?? []).map((schedule) => ( {localized(schedule.name, i18n.language) || schedule.code} {schedule.isDefault && ( default )} {schedule.crossesMidnight && ( night )} {!schedule.isActive && ( inactive )} {schedule.code} {schedule.startTime.slice(0, 5)} —{" "} {schedule.endTime.slice(0, 5)} {schedule.breakMinutes}m {schedule.gracePeriodMinutes}m {schedule.workingDays?.length ? ( {schedule.workingDays.map((day) => ( {DAY_NAMES[day]} ))} ) : ( company week )} {formatMinutes(schedule.minMinutesFullDay)} half at {formatMinutes(schedule.minMinutesHalfDay)} ))} {!schedules.isLoading && (schedules.data ?? []).length === 0 && ( No shift patterns yet. Without one, punches are recorded but lateness cannot be judged. )}
Proclamation 1156/2019 Art. 68 sets these as a floor. You may pay more; the API refuses less. {updateRate.isError && ( }> {apiErrorMessage(updateRate.error)} )} When Multiplier Cap per day Basis {(rates.data ?? []).map((rate) => ( {rate.category.replace("_", " ").toLowerCase()} { const value = event.currentTarget.value.replace("×", ""); if (Number(value) !== Number(rate.multiplier)) { updateRate.mutate({ id: rate.id, multiplier: Number(value).toFixed(2), }); } }} /> {rate.maxHoursPerDay ? `${Number(rate.maxHoursPerDay)}h` : "—"} {rate.statuteReference ?? "—"} ))} {(rates.data ?? []).length === 0 && ( No premiums configured — overtime cannot be claimed until there are. "Add statutory premiums" creates them. )}
setAdding(false)} /> ); } function AddScheduleModal({ opened, onClose, }: { opened: boolean; onClose: () => void; }) { const queryClient = useQueryClient(); const [code, setCode] = useState(""); const [name, setName] = useState({ am: "", en: "" }); const [startTime, setStartTime] = useState("08:30"); const [endTime, setEndTime] = useState("17:30"); const [breakMinutes, setBreakMinutes] = useState(60); const [grace, setGrace] = useState(10); const create = useMutation({ mutationFn: () => createWorkSchedule({ code, name, startTime, endTime, breakMinutes, gracePeriodMinutes: grace, // A shift finishing before it starts must be a night shift; saying so // here avoids a refusal the user cannot act on. crossesMidnight: endTime <= startTime, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["work-schedules"] }); setCode(""); setName({ am: "", en: "" }); onClose(); }, }); return ( {create.isError && ( }> {apiErrorMessage(create.error)} )} setCode(event.currentTarget.value.toUpperCase())} /> setStartTime(event.currentTarget.value)} /> setEndTime(event.currentTarget.value)} /> {endTime <= startTime && ( This ends before it starts, so it will be treated as a night shift running into the next day. )} setBreakMinutes(Number(value) || 0)} /> setGrace(Number(value) || 0)} /> ); }