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",
|
||||
|
||||
+119
-7
@@ -11,6 +11,9 @@ export const sq = {
|
||||
close: "Mbyll",
|
||||
save: "Ruaj",
|
||||
none: "—",
|
||||
themeDark: "errët",
|
||||
themeLight: "çelët",
|
||||
theme: "Tema",
|
||||
},
|
||||
auth: {
|
||||
title: "Sistemi i Parkimit",
|
||||
@@ -23,11 +26,13 @@ export const sq = {
|
||||
booth: "Kabina",
|
||||
shift: "Turni",
|
||||
setup: "Konfigurimi",
|
||||
devices: "Pajisjet",
|
||||
tariff: "Tarifa",
|
||||
subscriptions: "Abonimet",
|
||||
site: "Vendi",
|
||||
site: "Park",
|
||||
users: "Përdoruesit",
|
||||
roles: "Rolet",
|
||||
shifts: "Turnet",
|
||||
},
|
||||
status: {
|
||||
live: "LIVE",
|
||||
@@ -103,7 +108,7 @@ export const sq = {
|
||||
},
|
||||
tariff: {
|
||||
title: "Tarifa",
|
||||
noRateCard: "Asnjë kartë tarifore e publikuar — arka nuk mund të faturojë derisa të publikoni një.",
|
||||
noRateCard: "Asnjë tarifë e publikuar — arka nuk mund të faturojë derisa të publikoni një.",
|
||||
activeSince: "Aktive që nga {{date}} · {{count}} version(e) në histori. Publikimi krijon një version të ri; sesionet e kaluara ruajnë çmimin origjinal.",
|
||||
currency: "Monedha",
|
||||
freeEntryGrace: "Periudha pa pagesë në hyrje (min)",
|
||||
@@ -123,13 +128,13 @@ export const sq = {
|
||||
addBlock: "+ Shto bllok",
|
||||
publishNewVersion: "Publiko version të ri",
|
||||
publishing: "Duke publikuar…",
|
||||
publishedOk: "U publikua versioni i ri i tarifës — tani është karta tarifore aktive.",
|
||||
defaultCard: "Karta e parazgjedhur (gjithmonë aktive)",
|
||||
publishedOk: "U publikua versioni i ri i tarifës — tani është tarifa aktive.",
|
||||
defaultCard: "Tarifa bazë (gjithmonë aktive)",
|
||||
defaultCardHint: "Çmimi bazë i zbatuar kur asnjë nivel kohor/sezonal nuk vlen. Kjo e vetme është mjaftueshëm për shumicën e parkimeve.",
|
||||
modeLadder: "Shkallë orësh",
|
||||
modeFlat: "Çmim fiks",
|
||||
tiersAdvanced: "Të avancuara: nivele kohore & sezonale",
|
||||
tiersHint: "Opsionale. Shto karta që vlejnë vetëm në orë/ditë/data ose kategori të caktuara (p.sh. orë e lirë, tarifë nate, fundjavë, autobus). Pa nivele, publikohet karta e thjeshtë.",
|
||||
tiersHint: "Opsionale. Shto nivele tarifore që vlejnë vetëm në orë/ditë/data ose kategori të caktuara (p.sh. orë e lirë, tarifë nate, fundjavë, autobus). Pa nivele, publikohet vetëm tarifa bazë.",
|
||||
tierName: "Emri",
|
||||
tierPriority: "Përparësia",
|
||||
tierCategory: "Kategoria",
|
||||
@@ -147,6 +152,79 @@ export const sq = {
|
||||
dow6: "Sht",
|
||||
dow0: "Die",
|
||||
},
|
||||
setup: {
|
||||
title: "Konfigurimi",
|
||||
intro:
|
||||
"Shto fillimisht kontrolluesit e barrierave — cakto cili rele është hyrje/dalje dhe në cilin terminal është lidhur butoni i hyrjes. Pastaj shto lexues, kamera dhe printera dhe drejto secilin te barriera që shërben.",
|
||||
// Category titles + the singular noun used in buttons/modal titles.
|
||||
catControllers: "Kontrolluesit (barrierat + butoni i hyrjes)",
|
||||
catReaders: "Lexuesit (QR / RFID)",
|
||||
catCameras: "Kamerat (foto + targë)",
|
||||
catPrinters: "Printerat (bileta / vouchera)",
|
||||
nounController: "kontrollues",
|
||||
nounReader: "lexues",
|
||||
nounCamera: "kamerë",
|
||||
nounPrinter: "printer",
|
||||
add: "+ Shto {{noun}}",
|
||||
addAnother: "+ Shto edhe një {{noun}}",
|
||||
addTitle: "Shto {{noun}}",
|
||||
editTitle: "Ndrysho {{noun}}",
|
||||
needControllerFirst: "Shto fillimisht një kontrollues — {{noun}} drejtohet te një prej releve të tij.",
|
||||
failedToLoad: "Ngarkimi i konfigurimit dështoi: {{error}}",
|
||||
loadingCatalog: "Duke ngarkuar katalogun e pajisjeve…",
|
||||
// Direction labels (relay direction + inherited binding).
|
||||
dirEntry: "Hyrje",
|
||||
dirExit: "Dalje",
|
||||
dirBoth: "Hyrje + dalje",
|
||||
inherits: "trashëgon {{direction}}",
|
||||
// Warnings panel.
|
||||
warnTitle: "⚠ U ruajt, por nevojitet veprim:",
|
||||
dismiss: "Mbyll",
|
||||
// Assignment row.
|
||||
disabled: "(çaktivizuar)",
|
||||
edit: "Ndrysho",
|
||||
remove: "Hiq",
|
||||
removing: "Duke hequr…",
|
||||
confirmRemove: "Të hiqet kjo pajisje {{driver}}?",
|
||||
noRelaysSet: "asnjë rele e caktuar",
|
||||
unbound: "e palidhur",
|
||||
// Device form.
|
||||
noDrivers: "Asnjë drejtues i regjistruar.",
|
||||
chooseDevice: "Zgjidh një pajisje…",
|
||||
scan: "Skano për kontrollues",
|
||||
scanning: "Duke skanuar…",
|
||||
noControllersFound: "Asnjë kontrollues në LAN.",
|
||||
use: "Përdor",
|
||||
test: "Testo lidhjen",
|
||||
testing: "Duke testuar…",
|
||||
saveConfigure: "Ruaj & konfiguro",
|
||||
saveChanges: "Ruaj ndryshimet",
|
||||
saving: "Duke ruajtur…",
|
||||
cancel: "Anulo",
|
||||
testFailed: "Testi dështoi: {{error}}",
|
||||
saveFailed: "Ruajtja dështoi: {{error}}",
|
||||
deviceLabel: "Pajisja:",
|
||||
preconditionsOk: "● parakushtet OK",
|
||||
autoFixedOnSave: "(rregullohet vetë në ruajtje)",
|
||||
backendPushIp: "IP-ja e backend-it",
|
||||
chooseAddress: "Zgjidh një adresë…",
|
||||
onDeviceSubnet: "— në subnetin e pajisjes",
|
||||
noNicOnSubnet: "⚠ asnjë NIC në subnetin e pajisjes — pajisja mund të mos arrijë backend-in",
|
||||
backendIpHint: "Adresa te e cila kjo pajisje do të dërgojë eventet e hyrjes.",
|
||||
// Relay editor.
|
||||
relaysTitle: "Relet në këtë kontrollues",
|
||||
relaysHint:
|
||||
"Çdo rele hap një barrierë. Cakto drejtimin e saj; për hyrje kalimtare, cakto në cilin terminal hyrës është lidhur butoni i hyrjes.",
|
||||
relay: "Rele",
|
||||
entryButtonTerminal: "Butoni i hyrjes në terminalin",
|
||||
addRelay: "+ Shto rele",
|
||||
// Binding picker.
|
||||
whichBarrier: "Cilën barrierë shërben kjo pajisje?",
|
||||
controller: "Kontrolluesi",
|
||||
choose: "Zgjidh…",
|
||||
relayLabel: "Rele {{relay}} ({{direction}})",
|
||||
noRelaysConfigured: "Ky kontrollues nuk ka rele të konfiguruar.",
|
||||
},
|
||||
subs: {
|
||||
title: "Abonimet",
|
||||
unnamed: "(pa emër)",
|
||||
@@ -182,8 +260,8 @@ export const sq = {
|
||||
commaSeparatedOptional: "të ndara me presje (opsionale)",
|
||||
credentials: "Kredencialet",
|
||||
credentialsCardQr: "Kredencialet (kartë / QR)",
|
||||
rfCardTag: "Kartë/etiketë RF",
|
||||
rfCardTagSoon: "Kartë/etiketë RF (së shpejti)",
|
||||
rfCardTag: "Kartë/Tag RF",
|
||||
rfCardTagSoon: "Kartë/Tag RF (së shpejti)",
|
||||
rfPlaceholder: "numri i kartës (ose lexo kartën)",
|
||||
readCard: "Lexo kartën",
|
||||
captureChooseReader: "Zgjidh lexuesin, pastaj afro kartën:",
|
||||
@@ -242,8 +320,17 @@ export const sq = {
|
||||
newPassword: "fjalëkalim i ri",
|
||||
role: "Roli",
|
||||
resetPassword: "Rivendos fjalëkalimin",
|
||||
edit: "Ndrysho",
|
||||
editTitle: "Ndrysho përdoruesin",
|
||||
save: "Ruaj",
|
||||
delete: "Fshi",
|
||||
confirmDelete: "Të fshihet përdoruesi \"{{name}}\"?",
|
||||
// Optional profile metadata.
|
||||
detailsSection: "Të dhënat (opsionale)",
|
||||
fullName: "Emri i plotë",
|
||||
phone: "Telefoni",
|
||||
email: "Email",
|
||||
address: "Adresa",
|
||||
},
|
||||
roles: {
|
||||
title: "Rolet",
|
||||
@@ -306,6 +393,31 @@ export const sq = {
|
||||
openNow: "Hap turnin tani",
|
||||
opening: "Duke hapur…",
|
||||
},
|
||||
shifts: {
|
||||
title: "Historiku i turneve",
|
||||
myTitle: "Turnet e mia",
|
||||
none: "Asnjë turn i mbyllur.",
|
||||
operator: "Operatori",
|
||||
started: "Filloi",
|
||||
ended: "Mbaroi",
|
||||
payments: "Pagesa",
|
||||
cash: "Para",
|
||||
card: "Kartë",
|
||||
expectedDrawer: "Arka e pritshme",
|
||||
// Filter (admin only).
|
||||
filterFrom: "Nga",
|
||||
filterTo: "Deri",
|
||||
allOperators: "Të gjithë operatorët",
|
||||
apply: "Apliko",
|
||||
clear: "Pastro",
|
||||
// Expanded drawer detail.
|
||||
drawerSection: "Arka",
|
||||
openingFloat: "Bilanci fillestar",
|
||||
cashTaken: "Para të marra",
|
||||
cashAdded: "Para të shtuara",
|
||||
cashRemoved: "Para të hequra",
|
||||
loadFailed: "Ngarkimi i turneve dështoi.",
|
||||
},
|
||||
pay: {
|
||||
ticket: "Bileta",
|
||||
entry: "Hyrja",
|
||||
|
||||
Reference in New Issue
Block a user