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:
+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