feat(oxibadge): cache employee names locally and let readers be a list
Check, format and test / build (push) Successful in 10m27s
Check, format and test / build (push) Successful in 10m27s
Employee names are now cached locally (employees.toml, keyed by reader
index then employee id) instead of being re-fetched from the time
clock on every month: load_employees only asks the device for ids not
already in the cache. When a month has employees the cache doesn't
know yet, a new screen (NewEmployees.svelte) lets the user name them
before continuing to the review screen; the chosen names are local to
Oxibadge only, never written back to the device. The same cache can be
managed afterwards from a drawer reachable from the time clock card in
Settings ("Gestisci dipendenti"), reusing the rename-in-row pattern
from the original mockup.
Settings.readers is a list again (was flattened to a single `reader`
object) so multiple time clocks can be supported later; only the first
one is used today.
Also:
- buildEmployees() now derives the employee list from the month's
records instead of from every cached name ever seen, so the review
screen no longer shows employees unrelated to the selected month.
- Renaming an employee from the review screen's pencil button now
actually persists to the cache (the callback existed but was never
wired up).
- Translated to English a number of Rust/TS/Svelte comments that had
been written in Italian; only the i18n locale files should contain
Italian.
- Minor dedup: extracted PillButton.svelte (was duplicated inline in
ReaderCard), promoted the repeated .mono CSS rule to app.css, moved
initials() out of the review feature into $lib/text.ts.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -147,10 +147,6 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.mono {
|
||||
font-family: var(--ox-font-mono);
|
||||
}
|
||||
|
||||
.table-box {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -249,7 +245,7 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Celle orario */
|
||||
/* Time cells */
|
||||
.slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -356,7 +352,7 @@
|
||||
background: rgba(77, 141, 255, 0.12);
|
||||
}
|
||||
|
||||
/* Note */
|
||||
/* Notes */
|
||||
td.note {
|
||||
font-size: 13px;
|
||||
color: #6e6e6e;
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<!-- Bottone diviso Esporta -->
|
||||
<!-- Split export button -->
|
||||
<div class="split-wrap">
|
||||
<div class="split">
|
||||
<button type="button" class="split-main" onclick={onexportall}>
|
||||
@@ -178,7 +178,7 @@
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
/* Bottone diviso */
|
||||
/* Split button */
|
||||
.split-wrap {
|
||||
position: relative;
|
||||
z-index: 21;
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
// Logica pura di analisi delle timbrature di un mese, estratta dal
|
||||
// componente di revisione per tenerlo leggibile. Le stringhe tradotte
|
||||
// vengono prodotte passando la funzione `t` (di solito `$_` di
|
||||
// svelte-i18n) e, dove serve la lingua per Intl, `locale` ($locale).
|
||||
// Pure logic for analyzing a month's attendance records, extracted from
|
||||
// the review component to keep it readable. Translated strings are
|
||||
// produced by passing the `t` function (usually svelte-i18n's `$_`) and,
|
||||
// where Intl needs the language, `locale` ($locale).
|
||||
|
||||
import { weekdayName } from '$lib/month';
|
||||
import type { Shifts } from '$lib/shifts';
|
||||
import { initials } from '$lib/text';
|
||||
import { fmt, formatTyped, hm, parseT } from '$lib/time';
|
||||
|
||||
export { formatTyped, hm, parseT };
|
||||
export { formatTyped, hm, initials, parseT };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- matches svelte-i18n's own loosely-typed `values`
|
||||
export type Translate = (key: string, options?: { values?: Record<string, any> }) => string;
|
||||
|
||||
/** [Entrata1[], Uscita1[], Entrata2[], Uscita2[]] — ogni casella è un
|
||||
* elenco di orari "HH:MM" per rappresentare anche le timbrature impilate. */
|
||||
/** [In1[], Out1[], In2[], Out2[]] — each slot is a list of "HH:MM" times,
|
||||
* to also represent stacked punches. */
|
||||
export type Day = [string[], string[], string[], string[]];
|
||||
|
||||
export interface Employee {
|
||||
@@ -78,7 +79,7 @@ export function dayWord(n: number, t: Translate): string {
|
||||
return t('review.day_word', { values: { n } });
|
||||
}
|
||||
|
||||
/** [entrata, uscita] se il giorno ha solo 2 timbrature che scavalcano la pausa pranzo. */
|
||||
/** [in, out] if the day has exactly 2 punches spanning the lunch break. */
|
||||
export function lunchSpan(day: Day, shift: Shifts): [string, string] | null {
|
||||
const all = day.flat().filter((x) => x !== '');
|
||||
if (all.length !== 2) return null;
|
||||
@@ -92,12 +93,12 @@ export function lunchSpan(day: Day, shift: Shifts): [string, string] | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ridistribuisce timbrature finite tutte nello stesso turno (es. per un
|
||||
* timbratore configurato male) nelle 4 caselle corrette, scegliendo per
|
||||
* ciascuna l'orario previsto (E1/U1/E2/U2) più vicino. Dato che gli orari
|
||||
* sono ordinati e i turni previsti pure, basta confrontare ogni timbratura
|
||||
* col punto a metà strada fra due turni consecutivi: è esattamente il
|
||||
* confine fra "più vicino al turno prima" e "più vicino al turno dopo".
|
||||
* Redistributes punches that all ended up in the same shift (e.g. from a
|
||||
* misconfigured time clock) into the correct 4 slots, picking for each one
|
||||
* the closest expected shift time (E1/U1/E2/U2). Since both the punches and
|
||||
* the expected shifts are sorted, it's enough to compare each punch against
|
||||
* the midpoint between two consecutive shifts: that's exactly the boundary
|
||||
* between "closer to the shift before" and "closer to the shift after".
|
||||
*/
|
||||
export function assignToShifts(times: string[], shift: Shifts): Day {
|
||||
const targets = [shift.in1, shift.out1, shift.in2, shift.out2].map((x) => parseT(x) as number);
|
||||
@@ -147,7 +148,7 @@ export function analyze(
|
||||
const miss: [boolean, boolean, boolean, boolean] = [false, false, false, false];
|
||||
let issue = '';
|
||||
|
||||
// 1. Turno sbagliato
|
||||
// 1. Wrong shift
|
||||
if (stacked) {
|
||||
const total = lists.reduce((n, c) => n + c.length, 0);
|
||||
const shiftLabel = filled[0] || filled[1] ? t('review.shift_1') : t('review.shift_2');
|
||||
@@ -156,9 +157,9 @@ export function analyze(
|
||||
if (!filled[i]) miss[i] = true;
|
||||
});
|
||||
}
|
||||
// 2. Orario non valido
|
||||
// 2. Invalid time
|
||||
if (!issue && bad.some((c) => c.some(Boolean))) issue = t('review.issue_invalid_time');
|
||||
// 3. Pausa pranzo non timbrata
|
||||
// 3. Unpunched lunch break
|
||||
const lunch = !stacked && !issue && lunchSpan(v, shift) !== null;
|
||||
if (lunch) {
|
||||
issue = t('review.issue_lunch_missing');
|
||||
@@ -166,7 +167,7 @@ export function analyze(
|
||||
if (!filled[i]) miss[i] = true;
|
||||
});
|
||||
}
|
||||
// 4-5. Coppie incomplete / uscita prima dell'entrata
|
||||
// 4-5. Incomplete pairs / clock-out before clock-in
|
||||
if (!stacked && !lunch) {
|
||||
const p = lists.map((c) => (c.length ? parseT(c[0]) : null));
|
||||
for (const [a, b, shiftKey] of [
|
||||
@@ -186,7 +187,7 @@ export function analyze(
|
||||
}
|
||||
}
|
||||
|
||||
// Ore: coppie per posizione dentro ciascun turno
|
||||
// Hours: pairs by position within each shift
|
||||
let mins = 0;
|
||||
for (const [a, b] of [
|
||||
[0, 1],
|
||||
@@ -206,7 +207,7 @@ export function analyze(
|
||||
const wasStacked = o.some((c) => c.length > 1);
|
||||
const wasLunch = !lunch && lunchSpan(o, shift) !== null && v[1][0] === shift.out1 && v[2][0] === shift.in2;
|
||||
|
||||
// 6. Info: orari non arrotondati
|
||||
// 6. Info: unrounded times
|
||||
const unr = v.map((c) => c.map((x) => x !== '' && !isRounded(x, shift.round_step_min)));
|
||||
const hasUnr = unr.some((c) => c.some(Boolean));
|
||||
const info = !issue && hasUnr;
|
||||
@@ -232,7 +233,7 @@ export function dayLabel(r: Row): string {
|
||||
return `${r.dow} ${r.di + 1}`;
|
||||
}
|
||||
|
||||
/** "dip-giorno-casella-indice" -> è l'orario attuale il risultato di un arrotondamento? */
|
||||
/** "employee-day-slot-index" -> is the current time the result of a rounding? */
|
||||
export function isRnd(
|
||||
rounded: Record<string, string>,
|
||||
emp: number,
|
||||
@@ -271,16 +272,6 @@ export function cellClass(r: Row, ci: number, ii: number, rnd: boolean): string
|
||||
return c;
|
||||
}
|
||||
|
||||
export function initials(n: string): string {
|
||||
return n
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((w) => w[0])
|
||||
.join('')
|
||||
.slice(0, 2)
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
export function summary(iss: number, inf: number, t: Translate): string {
|
||||
return [iss ? `${dayWord(iss, t)} ${t('review.to_check')}` : '', inf ? `${dayWord(inf, t)} ${t('review.to_round')}` : '']
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Day, Employee } from './analysis';
|
||||
|
||||
type Clock = 'FirstIn' | 'FirstOut' | 'SecondIn' | 'SecondOut';
|
||||
|
||||
/** Un record come lo restituisce il comando Tauri `load_records`. */
|
||||
/** A record as returned by the `load_records` Tauri command. */
|
||||
export interface AttendanceRecord {
|
||||
employee_id: number;
|
||||
clock: Clock;
|
||||
@@ -21,7 +21,12 @@ function timeOf(datetime: string): string {
|
||||
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** Raggruppa i record per dipendente e giorno nella forma Day attesa da TimesheetReview. */
|
||||
/**
|
||||
* Groups records by employee and day into the Day shape expected by
|
||||
* TimesheetReview. Only employees with at least one record this month are
|
||||
* included — `names` is just used to resolve a display name, not to decide
|
||||
* who shows up (it may hold every employee ever cached, across all months).
|
||||
*/
|
||||
export function buildEmployees(
|
||||
year: number,
|
||||
month: number,
|
||||
@@ -31,13 +36,12 @@ export function buildEmployees(
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
const byEmployee = new Map<number, Day[]>();
|
||||
|
||||
for (const id of Object.keys(names)) {
|
||||
byEmployee.set(Number(id), Array.from({ length: daysInMonth }, () => [[], [], [], []] as Day));
|
||||
}
|
||||
|
||||
for (const record of records) {
|
||||
const days = byEmployee.get(record.employee_id);
|
||||
if (!days) continue;
|
||||
let days = byEmployee.get(record.employee_id);
|
||||
if (!days) {
|
||||
days = Array.from({ length: daysInMonth }, () => [[], [], [], []] as Day);
|
||||
byEmployee.set(record.employee_id, days);
|
||||
}
|
||||
const date = new Date(record.datetime);
|
||||
const day = days[date.getDate() - 1];
|
||||
day[CLOCK_INDEX[record.clock]].push(timeOf(record.datetime));
|
||||
|
||||
Reference in New Issue
Block a user