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:
@@ -1,11 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { error, setError, clearError } from '$lib/error';
|
||||
import type { Settings } from '$lib/settings';
|
||||
import { PRIMARY_READER_INDEX, type EmployeeCache, type EmployeeNames } from '$lib/employees';
|
||||
import Field from '../Field.svelte';
|
||||
import ErrorBanner from '../ErrorBanner.svelte';
|
||||
import ActionButton from '../review/ActionButton.svelte';
|
||||
import PillButton from '../PillButton.svelte';
|
||||
import EmployeesDrawer from './EmployeesDrawer.svelte';
|
||||
import { IPV4 } from './logic';
|
||||
|
||||
let { host, port, onsave }: { host: string; port: number; onsave: (host: string, port: number) => void } =
|
||||
@@ -18,6 +22,48 @@
|
||||
|
||||
let status = $state<'' | 'testing' | 'ok'>('');
|
||||
|
||||
let fullEmployeeCache = $state<EmployeeCache>({});
|
||||
let employeeCache = $state<EmployeeNames>({});
|
||||
let employeesOpen = $state(false);
|
||||
let employeeQuery = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
fullEmployeeCache = await invoke<EmployeeCache>('load_employee_cache');
|
||||
employeeCache = fullEmployeeCache[PRIMARY_READER_INDEX] ?? {};
|
||||
});
|
||||
|
||||
const employeesList = $derived(
|
||||
Object.entries(employeeCache)
|
||||
.map(([id, name]) => ({ id, name }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
const employeeCount = $derived(employeesList.length);
|
||||
const visibleEmployees = $derived(
|
||||
employeesList.filter(
|
||||
(e) =>
|
||||
!employeeQuery.trim() ||
|
||||
e.name.toLowerCase().includes(employeeQuery.trim().toLowerCase()) ||
|
||||
e.id.includes(employeeQuery.trim())
|
||||
)
|
||||
);
|
||||
|
||||
async function persistEmployees(next: EmployeeNames) {
|
||||
employeeCache = next;
|
||||
const nextFullCache = { ...fullEmployeeCache, [PRIMARY_READER_INDEX]: next };
|
||||
fullEmployeeCache = nextFullCache;
|
||||
await invoke('save_employee_cache', { cache: nextFullCache });
|
||||
}
|
||||
|
||||
function renameEmployee(id: string, name: string) {
|
||||
persistEmployees({ ...employeeCache, [id]: name });
|
||||
}
|
||||
|
||||
function removeEmployee(id: string) {
|
||||
const next = { ...employeeCache };
|
||||
delete next[id];
|
||||
persistEmployees(next);
|
||||
}
|
||||
|
||||
function startEdit() {
|
||||
draftHost = host;
|
||||
draftPort = port;
|
||||
@@ -43,7 +89,7 @@
|
||||
clearError();
|
||||
try {
|
||||
const settings = await invoke<Settings>('load_settings');
|
||||
await invoke('save_settings', { settings: { ...settings, reader: { host: v, port: nextPort } } });
|
||||
await invoke('save_settings', { settings: { ...settings, readers: [{ host: v, port: nextPort }] } });
|
||||
onsave(v, nextPort);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
@@ -106,18 +152,33 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="dev-actions">
|
||||
<button type="button" class="pill" onclick={startEdit}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true"
|
||||
><path d="M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16v4z" /><path d="M13.5 6.5l4 4" /></svg
|
||||
>
|
||||
<span>{$_('settings.edit_address')}</span>
|
||||
</button>
|
||||
<button type="button" class="pill" onclick={test} disabled={status === 'testing'}>
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true"
|
||||
><path d="M5 12.5a10 10 0 0 1 14 0M8.5 16a5 5 0 0 1 7 0M12 19.5h.01" /></svg
|
||||
>
|
||||
<span>{$_('settings.test_connection')}</span>
|
||||
</button>
|
||||
<PillButton onclick={startEdit}>
|
||||
{#snippet icon()}
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true"
|
||||
><path d="M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16v4z" /><path d="M13.5 6.5l4 4" /></svg
|
||||
>
|
||||
{/snippet}
|
||||
{$_('settings.edit_address')}
|
||||
</PillButton>
|
||||
<PillButton onclick={test} disabled={status === 'testing'}>
|
||||
{#snippet icon()}
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true"
|
||||
><path d="M5 12.5a10 10 0 0 1 14 0M8.5 16a5 5 0 0 1 7 0M12 19.5h.01" /></svg
|
||||
>
|
||||
{/snippet}
|
||||
{$_('settings.test_connection')}
|
||||
</PillButton>
|
||||
<PillButton onclick={() => (employeesOpen = true)}>
|
||||
{#snippet icon()}
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true"
|
||||
><circle cx="9" cy="8" r="3.5" /><path d="M2.5 20c.8-3.3 3.3-5 6.5-5s5.7 1.7 6.5 5" /><path
|
||||
d="M16 4.5a3.5 3.5 0 0 1 0 7M18 15.2c1.8.7 3 2.3 3.5 4.8"
|
||||
/></svg
|
||||
>
|
||||
{/snippet}
|
||||
{$_('settings.manage_employees')}
|
||||
<span class="count mono">{employeeCount}</span>
|
||||
</PillButton>
|
||||
</div>
|
||||
{:else}
|
||||
<form
|
||||
@@ -162,11 +223,18 @@
|
||||
<ErrorBanner title={$_('errors.title')} message={$error} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.mono {
|
||||
font-family: var(--ox-font-mono);
|
||||
}
|
||||
{#if employeesOpen}
|
||||
<EmployeesDrawer
|
||||
employees={visibleEmployees}
|
||||
total={employeeCount}
|
||||
bind:query={employeeQuery}
|
||||
onclose={() => (employeesOpen = false)}
|
||||
onrename={renameEmployee}
|
||||
ondelete={removeEmployee}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.spin {
|
||||
animation: spin 0.9s linear infinite;
|
||||
}
|
||||
@@ -256,40 +324,10 @@
|
||||
border-top: 1px solid #222222;
|
||||
}
|
||||
|
||||
.pill {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
border-radius: 9px;
|
||||
border: 1px solid #333333;
|
||||
color: #c9c9c9;
|
||||
font-size: 13.5px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
border-color 0.15s;
|
||||
}
|
||||
|
||||
.pill:hover:not(:disabled) {
|
||||
background: #1e1e1e;
|
||||
color: #ffffff;
|
||||
border-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.pill:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.pill:focus-visible {
|
||||
outline: 2px solid var(--ox-accent);
|
||||
outline-offset: 2px;
|
||||
.count {
|
||||
margin-left: 6px;
|
||||
font-size: 12.5px;
|
||||
color: #6e6e6e;
|
||||
}
|
||||
|
||||
.dev-edit {
|
||||
|
||||
Reference in New Issue
Block a user