feat: tabbed setup, user metadata, light theme, scoped shift history
Consolidate the config screens under a single /setup hub with permission- gated tabs (Devices/Tariff/Subscriptions/Site/Users/Roles/Shifts), collapsing the top nav to Booth·Shift·Setup; old top-level paths redirect. Users: add optional profile metadata (full name, phone, email, address) on create/edit. Theme: a light palette saved to the user's profile (users.theme), toggled in the header beside the language switch and applied on load like the language preference. Both ride on a single additive migration (0008). Shift history: a new GET /api/shifts folds the signed shift_z_report chain into completed shifts, SCOPED server-side — operators see only their own; holders of shift:cash see all with an operator + date-range filter. Surfaced as the Shifts tab; an operator cannot read another operator's takings (param spoofing is ignored). These three features share the router, api client and i18n catalogs, so they land together. Verified live: theme persists across reload, metadata round- trips to the DB, and shift scoping holds (operator self-only, admin all+filter). Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
+106
-4
@@ -11,6 +11,9 @@ export const en: Catalog = {
|
||||
close: "Close",
|
||||
save: "Save",
|
||||
none: "—",
|
||||
themeDark: "dark",
|
||||
themeLight: "light",
|
||||
theme: "Theme",
|
||||
},
|
||||
auth: {
|
||||
title: "Parking System",
|
||||
@@ -23,11 +26,13 @@ export const en: Catalog = {
|
||||
booth: "Booth",
|
||||
shift: "Shift",
|
||||
setup: "Setup",
|
||||
devices: "Devices",
|
||||
tariff: "Tariff",
|
||||
subscriptions: "Subscriptions",
|
||||
site: "Site",
|
||||
users: "Users",
|
||||
roles: "Roles",
|
||||
shifts: "Shifts",
|
||||
},
|
||||
status: {
|
||||
live: "LIVE",
|
||||
@@ -101,7 +106,7 @@ export const en: Catalog = {
|
||||
},
|
||||
tariff: {
|
||||
title: "Tariff",
|
||||
noRateCard: "No rate card published yet — the pay station can't charge until you publish one.",
|
||||
noRateCard: "No tariff published yet — the pay station can't charge until you publish one.",
|
||||
activeSince: "Active since {{date}} · {{count}} version(s) in history. Publishing creates a new version; past sessions keep their original pricing.",
|
||||
currency: "Currency",
|
||||
freeEntryGrace: "Free entry grace (min)",
|
||||
@@ -121,13 +126,13 @@ export const en: Catalog = {
|
||||
addBlock: "+ Add block",
|
||||
publishNewVersion: "Publish new version",
|
||||
publishing: "Publishing…",
|
||||
publishedOk: "New tariff version published — it's now the active rate card.",
|
||||
defaultCard: "Default card (always active)",
|
||||
publishedOk: "New tariff version published — it's now the active rate.",
|
||||
defaultCard: "Base rate (always active)",
|
||||
defaultCardHint: "The base rate applied when no time/seasonal tier matches. This alone is enough for most car parks.",
|
||||
modeLadder: "Hourly ladder",
|
||||
modeFlat: "Flat price",
|
||||
tiersAdvanced: "Advanced: time & seasonal tiers",
|
||||
tiersHint: "Optional. Add cards that apply only at certain hours/days/dates or for a category (e.g. happy hour, night rate, weekend, bus). With no tiers, the simple card is published.",
|
||||
tiersHint: "Optional. Add tiers that apply only at certain hours/days/dates or for a category (e.g. happy hour, night rate, weekend, bus). With no tiers, just the base rate is published.",
|
||||
tierName: "Name",
|
||||
tierPriority: "Priority",
|
||||
tierCategory: "Category",
|
||||
@@ -145,6 +150,72 @@ export const en: Catalog = {
|
||||
dow6: "Sat",
|
||||
dow0: "Sun",
|
||||
},
|
||||
setup: {
|
||||
title: "Setup",
|
||||
intro:
|
||||
"Add your barrier controllers first — set which relay is entry/exit and which terminal the entry button is wired to. Then add readers, cameras and printers and point each at the barrier it serves.",
|
||||
catControllers: "Controllers (barriers + entry button)",
|
||||
catReaders: "Readers (QR / RFID)",
|
||||
catCameras: "Cameras (snapshot + plate)",
|
||||
catPrinters: "Printers (tickets / vouchers)",
|
||||
nounController: "controller",
|
||||
nounReader: "reader",
|
||||
nounCamera: "camera",
|
||||
nounPrinter: "printer",
|
||||
add: "+ Add {{noun}}",
|
||||
addAnother: "+ Add another {{noun}}",
|
||||
addTitle: "Add {{noun}}",
|
||||
editTitle: "Edit {{noun}}",
|
||||
needControllerFirst: "Add a controller first — a {{noun}} points at one of its relays.",
|
||||
failedToLoad: "Failed to load setup: {{error}}",
|
||||
loadingCatalog: "Loading device catalog…",
|
||||
dirEntry: "Entry",
|
||||
dirExit: "Exit",
|
||||
dirBoth: "Both (entry + exit)",
|
||||
inherits: "inherits {{direction}}",
|
||||
warnTitle: "⚠ Saved, but action needed:",
|
||||
dismiss: "Dismiss",
|
||||
disabled: "(disabled)",
|
||||
edit: "Edit",
|
||||
remove: "Remove",
|
||||
removing: "Removing…",
|
||||
confirmRemove: "Remove this {{driver}} device?",
|
||||
noRelaysSet: "no relays set",
|
||||
unbound: "unbound",
|
||||
noDrivers: "No drivers registered.",
|
||||
chooseDevice: "Choose a device…",
|
||||
scan: "Scan for controllers",
|
||||
scanning: "Scanning…",
|
||||
noControllersFound: "No controllers found on the LAN.",
|
||||
use: "Use",
|
||||
test: "Test connection",
|
||||
testing: "Testing…",
|
||||
saveConfigure: "Save & configure",
|
||||
saveChanges: "Save changes",
|
||||
saving: "Saving…",
|
||||
cancel: "Cancel",
|
||||
testFailed: "Test failed: {{error}}",
|
||||
saveFailed: "Save failed: {{error}}",
|
||||
deviceLabel: "Device:",
|
||||
preconditionsOk: "● preconditions OK",
|
||||
autoFixedOnSave: "(auto-fixed on save)",
|
||||
backendPushIp: "Backend push IP",
|
||||
chooseAddress: "Choose an address…",
|
||||
onDeviceSubnet: "— on device subnet",
|
||||
noNicOnSubnet: "⚠ no NIC on the device's subnet — the device may not reach the backend",
|
||||
backendIpHint: "The address this device will POST input events to.",
|
||||
relaysTitle: "Relays on this controller",
|
||||
relaysHint:
|
||||
"Each relay opens one barrier. Set its direction; for transient entry, set which input terminal the entry button is wired to.",
|
||||
relay: "Relay",
|
||||
entryButtonTerminal: "Entry button on terminal",
|
||||
addRelay: "+ Add relay",
|
||||
whichBarrier: "Which barrier does this device serve?",
|
||||
controller: "Controller",
|
||||
choose: "Choose…",
|
||||
relayLabel: "Relay {{relay}} ({{direction}})",
|
||||
noRelaysConfigured: "This controller has no relays configured.",
|
||||
},
|
||||
subs: {
|
||||
title: "Subscriptions",
|
||||
unnamed: "(unnamed)",
|
||||
@@ -240,8 +311,16 @@ export const en: Catalog = {
|
||||
newPassword: "new password",
|
||||
role: "Role",
|
||||
resetPassword: "Reset password",
|
||||
edit: "Edit",
|
||||
editTitle: "Edit user",
|
||||
save: "Save",
|
||||
delete: "Delete",
|
||||
confirmDelete: "Delete user \"{{name}}\"?",
|
||||
detailsSection: "Details (optional)",
|
||||
fullName: "Full name",
|
||||
phone: "Phone",
|
||||
email: "Email",
|
||||
address: "Address",
|
||||
},
|
||||
roles: {
|
||||
title: "Roles",
|
||||
@@ -304,6 +383,29 @@ export const en: Catalog = {
|
||||
openNow: "Open shift now",
|
||||
opening: "Opening…",
|
||||
},
|
||||
shifts: {
|
||||
title: "Shift history",
|
||||
myTitle: "My shifts",
|
||||
none: "No closed shifts.",
|
||||
operator: "Operator",
|
||||
started: "Started",
|
||||
ended: "Ended",
|
||||
payments: "Payments",
|
||||
cash: "Cash",
|
||||
card: "Card",
|
||||
expectedDrawer: "Expected drawer",
|
||||
filterFrom: "From",
|
||||
filterTo: "To",
|
||||
allOperators: "All operators",
|
||||
apply: "Apply",
|
||||
clear: "Clear",
|
||||
drawerSection: "Drawer",
|
||||
openingFloat: "Opening float",
|
||||
cashTaken: "Cash taken",
|
||||
cashAdded: "Cash added",
|
||||
cashRemoved: "Cash removed",
|
||||
loadFailed: "Failed to load shifts.",
|
||||
},
|
||||
pay: {
|
||||
ticket: "Ticket",
|
||||
entry: "Entry",
|
||||
|
||||
Reference in New Issue
Block a user