PatternHoursBreakGraceDaysFull 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.
)}
}
loading={seed.isPending}
onClick={() => seed.mutate()}
>
Add statutory premiums
Proclamation 1156/2019 Art. 68 sets these as a floor.
You may pay more; the API refuses less.
{updateRate.isError && (
}>
{apiErrorMessage(updateRate.error)}
)}
WhenMultiplierCap per dayBasis
{(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)}
/>
);
}