feat(oxibadge): add settings screen for shifts, rounding and language
Check, format and test / build (push) Successful in 10m22s

Add a settings screen (gear button on the month-confirmation screen)
to configure the expected shift times, the rounding step, the
interface language, and the time clock address — replacing the old
onboarding-only connection flow. Settings persist to the same
preferences.toml as the reader address, under a single load_settings/
save_settings command pair.

The review screen's "fix shifts" action now redistributes stacked
punches by nearest configured shift time instead of blind position,
and a stacked cell manually cleared down to one entry no longer
leaves a dangling empty input.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 18:29:57 +02:00
parent acd3efeac6
commit b65e85f908
26 changed files with 1397 additions and 118 deletions
@@ -0,0 +1,42 @@
import { parseT } from '$lib/time';
import type { Shifts } from '$lib/shifts';
export type { Shifts };
export const IPV4 = /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
/** Le 4 caselle orario di `Shifts` — a parte `round_step_min`, che non è un orario. */
export type ShiftTimeKey = 'in1' | 'out1' | 'in2' | 'out2';
export const SHIFT_ROWS: { titleKey: string; a: ShiftTimeKey; b: ShiftTimeKey }[] = [
{ titleKey: 'settings.shift_1', a: 'in1', b: 'out1' },
{ titleKey: 'settings.shift_2', a: 'in2', b: 'out2' }
];
export interface ShiftCheck {
minutes: Record<ShiftTimeKey, number> | null;
bad: Partial<Record<ShiftTimeKey, true>>;
errorKey: string | null;
}
export function checkShifts(shifts: Shifts): ShiftCheck {
const t: Record<ShiftTimeKey, number | null> = {
in1: parseT(shifts.in1),
out1: parseT(shifts.out1),
in2: parseT(shifts.in2),
out2: parseT(shifts.out2)
};
const bad: Partial<Record<ShiftTimeKey, true>> = {};
for (const key of Object.keys(t) as ShiftTimeKey[]) {
if (t[key] === null) bad[key] = true;
}
if (Object.keys(bad).length) return { minutes: null, bad, errorKey: 'settings.shift_error_format' };
const n = t as Record<ShiftTimeKey, number>;
if (n.out1 <= n.in1) return { minutes: null, bad: { out1: true }, errorKey: 'settings.shift_error_out1' };
if (n.in2 < n.out1) return { minutes: null, bad: { in2: true }, errorKey: 'settings.shift_error_in2' };
if (n.out2 <= n.in2) return { minutes: null, bad: { out2: true }, errorKey: 'settings.shift_error_out2' };
return { minutes: n, bad: {}, errorKey: null };
}
export const roundValid = (v: string): boolean => /^\d{1,2}$/.test(v) && Number(v) >= 1 && Number(v) <= 60;