From 5dbb3f51f1197c3ea6c730b78236836adef18db4 Mon Sep 17 00:00:00 2001 From: Nicola Belluti Date: Fri, 9 Oct 2026 15:21:47 +0200 Subject: [PATCH] feat(oxibadge): consolidate review toolbar and add a "shift times" action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-employee correction buttons (Fix shifts, Add lunch break, Round) and the day filter used to be scattered across the employee header. They're now grouped into a single toolbar attached to the top of the timesheet table: a "Corrections" dropdown (with per-item counts and an "Apply all corrections" entry) and a "Shift times" panel, plus the existing day filter and "Undo all changes" button. "Shift times" adds a fixed offset (in steps of the configured rounding granularity, ±12h) to every punch in a chosen day range, for either the current employee or everyone — for when the time clock didn't follow a daylight-saving change. EmployeeHeader is now just the name editor and stats; TimesheetTable no longer owns the table's outer border/background, which moved to the new shared card wrapper. Assisted-by: Claude Sonnet 5 --- .../src/lib/components/TimesheetReview.svelte | 87 +- .../components/review/EmployeeHeader.svelte | 132 +-- .../components/review/TimesheetTable.svelte | 4 +- .../src/lib/components/review/Toolbar.svelte | 778 ++++++++++++++++++ .../src/lib/components/review/analysis.ts | 14 + oxibadge/src/lib/locales/en.json | 21 +- oxibadge/src/lib/locales/it.json | 21 +- 7 files changed, 901 insertions(+), 156 deletions(-) create mode 100644 oxibadge/src/lib/components/review/Toolbar.svelte diff --git a/oxibadge/src/lib/components/TimesheetReview.svelte b/oxibadge/src/lib/components/TimesheetReview.svelte index 6acb283..4d96524 100644 --- a/oxibadge/src/lib/components/TimesheetReview.svelte +++ b/oxibadge/src/lib/components/TimesheetReview.svelte @@ -14,12 +14,14 @@ lunchSpan, normDay, roundT, + shiftT, shortSummary, summary } from './review/analysis'; import TopBar from './review/TopBar.svelte'; import EmployeeSidebar from './review/EmployeeSidebar.svelte'; import EmployeeHeader from './review/EmployeeHeader.svelte'; + import Toolbar from './review/Toolbar.svelte'; import TimesheetTable from './review/TimesheetTable.svelte'; import ExportModal from './review/ExportModal.svelte'; import Toast from './review/Toast.svelte'; @@ -57,6 +59,7 @@ /** "employee-day-slot-index" -> time produced by rounding */ let rounded = $state>({}); let hasUnexportedChanges = $state(false); + const daysInMonth = orig[0]?.length ?? 0; $effect(() => { ondirtychange?.(hasUnexportedChanges); @@ -135,6 +138,28 @@ hasUnexportedChanges = true; } + /** Applies all three automatic corrections to the current employee in order: shifts, then lunch, then rounding. */ + function applyAllCorrections() { + const allDays = values[emp].map((_d, di) => di); + fixDays(cur.stackedDays); + addLunch(allDays); + roundDaysOf(emp, allDays); + } + + /** Adds a fixed offset to every time of the chosen days, for the given employee(s) — e.g. to fix a + * daylight-saving change the time clock didn't follow. */ + function applyShift(opts: { minutes: number; fromDay: number; toDay: number; scope: 'all' | 'one' }) { + const eis = opts.scope === 'all' ? names.map((_name, i) => i) : [emp]; + const from = opts.fromDay - 1; + const to = opts.toDay - 1; + for (const ei of eis) { + for (let di = from; di <= to && di < values[ei].length; di++) { + values[ei][di] = values[ei][di].map((c) => c.map((x) => shiftT(x, opts.minutes))) as Day; + } + } + hasUnexportedChanges = true; + } + /** Reverts one day back to what the time clock originally reported. */ function undoDay(di: number) { values[emp][di] = orig[emp][di].map((c) => [...c]) as Day; @@ -330,28 +355,40 @@ issues={cur.issues} infos={cur.infos} edits={cur.edits} - stackedCount={cur.stackedDays.length} - lunchCount={cur.lunchDays.length} - roundCount={cur.roundDays.length} - {shift} - onfixall={() => fixDays(cur.stackedDays)} - onaddlunchall={() => addLunch(cur.lunchDays)} - onroundall={() => roundDaysOf(emp, cur.roundDays)} - onundoall={requestUndoAll} - bind:onlyIssues /> - fixDays([di])} - onaddlunch={(di) => addLunch([di])} - onroundday={(di) => roundDaysOf(emp, [di])} - onundoday={undoDay} - /> +
+ fixDays(cur.stackedDays)} + onaddlunchall={() => addLunch(cur.lunchDays)} + onroundall={() => roundDaysOf(emp, cur.roundDays)} + onapplyall={applyAllCorrections} + onshift={applyShift} + edits={cur.edits} + onundoall={requestUndoAll} + /> + + fixDays([di])} + onaddlunch={(di) => addLunch([di])} + onroundday={(di) => roundDaysOf(emp, [di])} + onundoday={undoDay} + /> +
{/if} @@ -397,6 +434,16 @@ gap: 16px; } + .table-card { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + border: 1px solid #262626; + border-radius: 14px; + background: #141414; + } + .empty-month { flex: 1; display: flex; diff --git a/oxibadge/src/lib/components/review/EmployeeHeader.svelte b/oxibadge/src/lib/components/review/EmployeeHeader.svelte index f411de5..b2218b3 100644 --- a/oxibadge/src/lib/components/review/EmployeeHeader.svelte +++ b/oxibadge/src/lib/components/review/EmployeeHeader.svelte @@ -1,9 +1,6 @@
@@ -108,76 +86,11 @@ 0}>{edits} {$_('review.edited')}
- -
- {#if stackedCount} - - {#snippet icon()} - - {/snippet} - {$_('review.fix_shifts', { values: { days: dayWord(stackedCount, $_) } })} - - {/if} - {#if lunchCount} - - {#snippet icon()} - - {/snippet} - {$_('review.add_lunch', { values: { days: dayWord(lunchCount, $_) } })} - - {/if} - {#if roundCount} - - {#snippet icon()} - - {/snippet} - {$_('review.round_all', { values: { step: shift.round_step_min, days: dayWord(roundCount, $_) } })} - - {/if} -
- - -
- {#if edits} - - {#snippet icon()} - - {/snippet} - - {/if} -
diff --git a/oxibadge/src/lib/components/review/TimesheetTable.svelte b/oxibadge/src/lib/components/review/TimesheetTable.svelte index 811474d..bf046c5 100644 --- a/oxibadge/src/lib/components/review/TimesheetTable.svelte +++ b/oxibadge/src/lib/components/review/TimesheetTable.svelte @@ -166,9 +166,7 @@ flex: 1; min-height: 0; overflow: auto; - border: 1px solid #262626; - border-radius: 14px; - background: #141414; + border-radius: 0 0 14px 14px; } table { diff --git a/oxibadge/src/lib/components/review/Toolbar.svelte b/oxibadge/src/lib/components/review/Toolbar.svelte new file mode 100644 index 0000000..f5f334e --- /dev/null +++ b/oxibadge/src/lib/components/review/Toolbar.svelte @@ -0,0 +1,778 @@ + + + + +{#if toolPopOpen} + +{/if} + +
+
+ + +
+ +
+
+ + {#if shiftOpen} + + {/if} +
+ +
+ + {#if fixMenu} + + {/if} +
+ + {#if edits} + + + {#snippet icon()} + + {/snippet} + + {/if} +
+
+ + diff --git a/oxibadge/src/lib/components/review/analysis.ts b/oxibadge/src/lib/components/review/analysis.ts index 98cdf71..58086b7 100644 --- a/oxibadge/src/lib/components/review/analysis.ts +++ b/oxibadge/src/lib/components/review/analysis.ts @@ -75,6 +75,20 @@ export function roundT(t: string, roundStep: number): string { return fmt(Math.min(Math.round(m / roundStep) * roundStep, 24 * 60 - roundStep)); } +/** Adds `delta` minutes to `t`, clamped to the same day (00:00-23:59). */ +export function shiftT(t: string, delta: number): string { + const m = parseT(t); + if (m === null) return t; + return fmt(Math.max(0, Math.min(24 * 60 - 1, m + delta))); +} + +/** Formats a signed minute offset as e.g. "+1:30", "−0:45", "0:00". */ +export function signedHm(m: number): string { + const sign = m > 0 ? '+' : m < 0 ? '−' : ''; + const abs = Math.abs(m); + return sign + Math.floor(abs / 60) + ':' + String(abs % 60).padStart(2, '0'); +} + export function dayWord(n: number, t: Translate): string { return t('review.day_word', { values: { n } }); } diff --git a/oxibadge/src/lib/locales/en.json b/oxibadge/src/lib/locales/en.json index e1571bc..1329291 100644 --- a/oxibadge/src/lib/locales/en.json +++ b/oxibadge/src/lib/locales/en.json @@ -124,7 +124,26 @@ "undo_all_title": "Restore this employee's times to what the time clock originally reported", "undo_all_confirm_title": "Undo all changes?", "undo_all_confirm_message": "All edited times for this employee will revert to what the time clock reported. This can't be undone.", - "undo_all_confirm": "Undo all changes" + "undo_all_confirm": "Undo all changes", + "corrections": "Corrections", + "add_lunch_action": "Add lunch break", + "round_action": "Round to {step} min", + "apply_all_corrections": "Apply all corrections", + "shift_times": "Shift times", + "shift_panel_title": "Shift all the times", + "shift_panel_desc": "For when the time clock didn't follow a daylight-saving time change.", + "shift_amount_label": "By how much", + "shift_minus_aria": "Minus 15 minutes", + "shift_plus_aria": "Plus 15 minutes", + "shift_minus_hour": "−1 hour", + "shift_plus_hour": "+1 hour", + "shift_days_label": "Which days", + "shift_from": "From", + "shift_to": "to", + "shift_who_label": "For whom", + "shift_scope_all": "Everyone", + "shift_preview": "E.g. {from} → {to}", + "apply": "Apply" }, "settings": { "title": "Settings", diff --git a/oxibadge/src/lib/locales/it.json b/oxibadge/src/lib/locales/it.json index a78d921..11d9693 100644 --- a/oxibadge/src/lib/locales/it.json +++ b/oxibadge/src/lib/locales/it.json @@ -124,7 +124,26 @@ "undo_all_title": "Ripristina gli orari di questo dipendente a quelli letti dal timbratore", "undo_all_confirm_title": "Annullare tutte le modifiche?", "undo_all_confirm_message": "Tutti gli orari modificati per questo dipendente torneranno a quelli letti dal timbratore. L'operazione non si può annullare.", - "undo_all_confirm": "Annulla tutte le modifiche" + "undo_all_confirm": "Annulla tutte le modifiche", + "corrections": "Correzioni", + "add_lunch_action": "Aggiungi pausa pranzo", + "round_action": "Arrotonda a {step} min", + "apply_all_corrections": "Applica tutte le correzioni", + "shift_times": "Sposta orari", + "shift_panel_title": "Sposta tutti gli orari", + "shift_panel_desc": "Per quando il timbratore non ha seguito il cambio dell'ora legale.", + "shift_amount_label": "Di quanto", + "shift_minus_aria": "Meno 15 minuti", + "shift_plus_aria": "Più 15 minuti", + "shift_minus_hour": "−1 ora", + "shift_plus_hour": "+1 ora", + "shift_days_label": "Quali giorni", + "shift_from": "Dal", + "shift_to": "al", + "shift_who_label": "Per chi", + "shift_scope_all": "Tutti", + "shift_preview": "Es. {from} → {to}", + "apply": "Applica" }, "settings": { "title": "Impostazioni",