Replaces the export button's placeholder (which only dbg!-printed the
payload) with a real implementation: one employee exports a single
.ods file, more than one bundles a .zip of one .ods per employee,
written wherever the user picks via the native "save" dialog
(tauri-plugin-dialog).
The "hours worked" column is a live ODF formula with a cached fallback
value, not just a static number, so the file stays correct if edited
afterward in LibreOffice; the formula and the cached value both treat
a day's own missing punches as 0 in the subtraction, matching how a
day with only an entry and an exit (no lunch break) still nets out to
the right span. Column headers are sent by the frontend (translated
via its own i18n), not hardcoded in Rust. A stacked slot (more than
one punch the device recorded in the same shift) exports every punch,
one per line in the cell, instead of only the first.
Also closes a real gap surfaced while building this: the new-employees
screen already blocked "Continue" for a blank name, but not for a name
the device sent as invalid UTF-8 (lossily converted to text containing
the Unicode replacement character on the Rust side) — such a name is
now flagged and blocks continuing too, so every employee reaching the
review/export flow is guaranteed to have a real, displayable name.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
The "Edit address" field in Settings only accepted an IPv4 address
(IPV4.test), rejecting a bare hostname (e.g. "timbratore") or FQDN
(e.g. "timbratore.lan") even though the underlying connection (Rust's
ToSocketAddrs) already resolves those fine. Onboarding had no such
restriction. Replaced the IPv4-only check with isValidHost (IPv4 OR a
standard DNS hostname), dropped the numeric-only keyboard hint on that
field, and reworded the labels/placeholder error to not imply IP-only.
Also makes "Edit address" test the connection before accepting it,
instead of saving unconditionally: ActionButton gained a `disabled`
prop so the confirm/cancel buttons can be locked (with a spinner on
confirm) while the test is in flight, and the edit form now only
closes on a successful test, staying open with the raw error shown
otherwise.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
The review screen now tracks whether there are edits not yet exported
and asks for confirmation before discarding them: clicking back, or
closing the app window entirely (intercepted via onCloseRequested,
which needed a core:window:allow-destroy capability that wasn't
granted by core:default). The confirmation dialog (ConfirmDialog.svelte)
is generic, built on a shared ModalFrame extracted from ExportModal so
the two don't duplicate the backdrop/box styling.
Also adds undo: a per-row button (icon-only, left of the note, shown
when that day has edits) reverts just that day instantly, and a
per-employee "undo all" (icon in a red box, last among the header's
tools) reverts every day for the current employee after a confirmation
dialog, since that one can't be undone itself.
ActionButton moves out of review/ next to IconButton/PillButton, since
ConfirmDialog (outside the review feature) now uses it too.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
Add the month-review screen shown after records/employees load: an
editable per-employee timesheet table with issue/rounding detection,
bulk fixes (reorder stacked punches, add lunch break, round to step),
and an export flow (single employee or all) wired to a new Rust
export_timesheet command. Split into focused components under
review/ instead of one large file, with shared i18n keys and a new
export_timesheet IPC command.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
load_records/load_employees failures were only logged to the console
and silently reset the UI to the month picker. Errors now flow through
a shared store (src/lib/error.ts) into an ErrorBanner, reused by both
Onboarding and ConfirmMonth, with the raw message passed straight
through from Rust (no stable-code classification layer — these are
mostly plain I/O errors, not worth categorizing).
i18n stays grouped per screen as before, with error-related strings
split into their own top-level "errors" group.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Adds the screen after onboarding: pick a month/year (ConfirmMonth), show
loading screens while the device is queried, then download that month's
records and resolve each distinct employee's name, printing both to
stdout (debug pretty-print) before landing on the placeholder "done"
screen.
- r701: RecordIterator's boxed stream is now Send, required for it to
cross into a Tauri command's future.
- oxibadge: load_records/load_employees commands, sharing the existing
3-retry connect_with_retries helper. load_records uses skip_while/
take_while on the device's newest-first stream to grab only the
target month without walking its whole history, then a single
collect + in-place Vec::reverse() back to chronological order.
- Shared LoadingScreen/Intro components extracted so future screens
reuse the same look without duplicating markup/CSS.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Adds the first real screen: a setup form for the device's host/port,
tested against the real device (3 retries) and persisted to
preferences.toml before showing a placeholder "done" screen.
- Rust: test_connection/save_preferences/load_preferences commands;
preferences stored as [[reader]] in TOML so more readers can be
added later without a format change.
- Svelte: split the monolithic page into reusable components
(Screen, Column, Field, Button, ErrorBanner, Hint) under
src/lib/components, sharing design tokens from app.css.
- Automatic it/en i18n via svelte-i18n, locale detected from
navigator.language.
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Scaffolds the oxibadge crate/frontend as a new workspace member, stripped
down to a bare hello-world, and
extends the CI pipeline
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Moving the library into its own r701/ directory makes room for the
Tauri GUI client (Oxibadge) to be added as a sibling workspace member
in a following commit. edition/license/repository are now inherited
from the new workspace root instead of being duplicated per member.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- fake_records now generates a full year (2026) of classic workday
punches (09:00-13:00, 14:00-18:00) for the employees in fake_users;
this spans many 1024-byte blocks, exercising the multi-block
streaming path
- fake_users/fake_records are now public so tests can assert against
them instead of hardcoded expectations
- scoped #[allow(clippy::unwrap_used)] to only the functions that
actually use unwrap, instead of a blanket file-level allow
- added gets_total_record_count, streams_records, and
gets_none_for_unknown_employee tests
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tests/mock/mod.rs emulates the device's TCP protocol (ping, get_name,
get_total_record_count, get_record_bytes), grounded in the reverse
engineering notes at
https://nicolabelluti.me/posts/2024/05/studying-a-communication-protocol/,
so tests/e2e.rs can exercise the real R701 client against it instead of
needing a physical reader. Starts with one test (connect + ping);
more scenarios (get_name, record streaming, error cases) to follow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Added rustdoc comments to every public item (R701, Record, Clock,
RecordIterator and their methods/fields/variants), each with an
# Errors section where applicable, so the clippy::missing_errors_doc
allows are no longer needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- request() now reads exactly the expected response length with
read_exact instead of a single non-guaranteed fill_buf, fixing a
latent bug where a response split across multiple TCP packets could
be mistaken for malformed data; the per-method length checks are now
redundant and were removed
- sequence_number wraps explicitly via wrapping_add instead of
relying on implicit (and debug-vs-release inconsistent) overflow
behavior after 65536 requests
- get_name validates UTF-8 strictly instead of silently lossy-decoding
invalid bytes; on invalid data it now returns the FromUtf8Error
itself (byte-level \0 trimming moved after the UTF-8 check), letting
the caller choose to fail or fall back to a lossy rendering
- removed the unused Default derive from Clock and Record
- removed npm from the CI image (no JS/Node in this repo), fixed a
typo in a CI step name
- fixed the Cargo.toml license field to the actual SPDX identifier
(AGPL-3.0-or-later, per the notice at the end of LICENSE) instead of
the free-text "GNU AGPLv3.0"
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>