Add audit log, duplicate prevention, quick-add staff, role editing#6
Conversation
- export_log table tracks every Google Sheets export (id, calc_id, exported_at, exported_by) - Export endpoint logs each export and returns Export ID + timestamp - Sheets rows now include Staff ID, Exported At, Export ID columns for full audit trail - Re-exporting a calculation shows a confirmation dialog with the previous export timestamp - calculate/[id] page shows last export time and Export # below the button; warns on re-export - Quick-add staff form on /calculate screen (available to shift leads AND managers) - New person is auto-selected for the current shift after add - Staff list updates reactively without a full page reload - Duplicate-name disambiguation: shows #ID badge when two staff share the same name - Visible on /calculate staff list and /settings/staff roster - Staff role can now be changed inline from Settings → Staff Roster (changeRole action) - No longer requires remove + re-add to change FOH ↔ Bar ↔ Kitchen https://claude.ai/code/session_01Sa6KBxSzsg6gWDavRF9dke
There was a problem hiding this comment.
Pull request overview
This PR adds an export audit trail for Google Sheets exports and improves staff management UX by enabling quick staff creation, duplicate-name disambiguation, and inline role editing.
Changes:
- Introduces
export_logpersistence and threads Export ID / exported timestamp through the export API, UI, and Sheets rows. - Adds quick-add staff on
/calculatewith reactive list updates and duplicate-name ID badges. - Enables inline staff role changes from Settings → Staff Roster.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/settings/staff/+page.svelte | Shows duplicate-name ID badges; adds inline role change form per staff member. |
| src/routes/settings/staff/+page.server.ts | Adds changeRole action (manager-only) to update staff role. |
| src/routes/calculate/+page.svelte | Adds quick-add staff form, reactive staff list state, and duplicate-name ID badges. |
| src/routes/calculate/+page.server.ts | Adds addStaff action to insert staff from /calculate. |
| src/routes/calculate/[id]/+page.svelte | Adds re-export confirmation + last-export UI, shows staff IDs on distribution rows, updates local export log after export. |
| src/routes/calculate/[id]/+page.server.ts | Loads export log for a calculation. |
| src/routes/api/export/+server.ts | Creates export_log row per export, appends audit columns to Sheets rows, returns export metadata. |
| src/lib/server/sheets.ts | Extends sheet header row with audit columns. |
| src/lib/server/db.ts | Adds export_log table and ExportLogRow type. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <form method="POST" action="?/addStaff" use:enhance={({ cancel }) => { | ||
| if (!newName.trim()) { addError = 'Name is required'; cancel(); return; } | ||
| addingStaff = true; | ||
| addError = ''; | ||
| return async ({ result, update }) => { | ||
| addingStaff = false; | ||
| if (result.type === 'success' && result.data?.addedId) { | ||
| const newPerson: StaffRow = { | ||
| id: result.data.addedId as number, | ||
| name: newName.trim(), | ||
| role: newRole, | ||
| active: 1, | ||
| location_id: 1, | ||
| source: 'manual', | ||
| square_team_member_id: null, | ||
| }; | ||
| staff = [...staff, newPerson].sort((a, b) => a.role.localeCompare(b.role) || a.name.localeCompare(b.name)); | ||
| included = new Set([...included, newPerson.id]); | ||
| newName = ''; | ||
| showAddForm = false; | ||
| } else { | ||
| await update(); | ||
| } | ||
| }; |
There was a problem hiding this comment.
The quick-add form never surfaces server-side failures: in the non-success path you only call update() and addError remains empty, so users get no feedback if the action returns fail(400, { addError: ... }) (or any other failure). Consider handling result.type === 'failure' by pulling addError from result.data (and keeping the form open), and only calling update() for success/navigation as needed.
| const json = await res.json(); | ||
| exportMsg = res.ok ? '✓ Exported to Google Sheets' : `Error: ${json.message}`; | ||
| if (res.ok) { | ||
| exportMsg = `Exported (Export #${json.exportId})`; | ||
| // Update local export log so button reflects new state without a page reload | ||
| exportLog = [{ id: json.exportId, calculation_id: data.calc.id, exported_at: Math.floor(Date.now() / 1000), exported_by: null, location_id: 1 }, ...exportLog]; | ||
| } else { | ||
| exportMsg = `Error: ${json.message}`; | ||
| } |
There was a problem hiding this comment.
On successful export you synthesize a new exportLog entry using Date.now() and set exported_by: null, even though the server generates/stores exported_at (unixepoch) and exported_by (locals.user.id). This can make the UI's “Last exported …” display diverge from the actual audit log (clock skew, wrong user). Prefer using server-returned values (e.g., have the endpoint return the inserted export_log row with exported_at/exported_by), or re-fetch/exportLog after success.
| <!-- Role change --> | ||
| <form method="POST" action="?/changeRole" use:enhance | ||
| style="margin-top:0.35rem;display:flex;align-items:center;gap:0.5rem;"> | ||
| <input type="hidden" name="id" value={person.id} /> | ||
| <span style="font-size:0.75rem;color:var(--muted);">Role:</span> | ||
| <select name="role" class="input" | ||
| style="font-size:0.75rem;padding:0.2rem 0.5rem;width:auto;height:auto;"> | ||
| {#each ['FOH', 'Bar', 'Kitchen'] as r} | ||
| <option value={r} selected={person.role === r}>{r}</option> | ||
| {/each} | ||
| </select> | ||
| <button type="submit" | ||
| style="background:none;font-size:0.75rem;font-weight:600;color:var(--primary);"> | ||
| Change | ||
| </button> | ||
| </form> |
There was a problem hiding this comment.
The change-role action can return fail(400, { roleError: ... }) (and also 403/404), but the page doesn't display any feedback for this form. With use:enhance default handling, failures will populate form, but nothing renders it here—so role changes can silently fail. Consider rendering form.roleError near the role form and/or adding an enhance callback that sets a per-row error state.
| export const HEADER_ROW = [ | ||
| 'Date', 'Shift', 'Type', 'Calc ID', | ||
| 'Gross Tips', 'CC Fee Rate', 'CC Fees', 'Net Tips', | ||
| 'Kitchen %', 'Kitchen Pool', 'Liquor Sales', 'Bar Liquor %', 'Bar Pool', 'FOH Pool', | ||
| 'Name', 'Role', 'FOH Share', 'Bar Share', 'Kitchen Share', 'Total', | ||
| 'Staff ID', 'Exported At', 'Export ID', | ||
| ]; |
There was a problem hiding this comment.
appendToSheet only writes the header row when the sheet is empty. After adding new columns ('Staff ID', 'Exported At', 'Export ID'), existing spreadsheets will start receiving extra columns but their header row will remain the old version (no labels), which undermines the audit trail UX. Consider detecting an existing header row and updating/patching it when it’s missing these columns (or documenting that users must clear/update the header manually).
- Quick-add form now surfaces server-side addError on failure instead of silently calling update(); failure path reads result.data.addError - Export UI uses server-returned exportedAtUnix/exportedBy values instead of Date.now()/null so the displayed timestamp matches the audit log record exactly - Sheets.ts now reads row 1 before appending; if the header exists but is missing the new audit columns it patches row 1 in-place via PUT before appending data - Role-change form in staff settings now renders form.roleError so failures are visible rather than silently dropped https://claude.ai/code/session_01Sa6KBxSzsg6gWDavRF9dke
https://claude.ai/code/session_01Sa6KBxSzsg6gWDavRF9dke