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:
+58
-3
@@ -45,6 +45,7 @@ export class ApiError extends Error {
|
||||
// --- Auth -----------------------------------------------------------------
|
||||
|
||||
export type Lang = "sq" | "en";
|
||||
export type Theme = "dark" | "light";
|
||||
/** A `resource:action` permission string (the server is the source of truth for
|
||||
* the full grid; the role composer fetches it via /api/roles). */
|
||||
export type Permission = string;
|
||||
@@ -57,6 +58,10 @@ export interface SessionUser {
|
||||
permissions: Permission[];
|
||||
/** Preferred UI language (loaded from the server on login). */
|
||||
language: Lang;
|
||||
/** Preferred UI theme (loaded from the server on login). */
|
||||
theme: Theme;
|
||||
/** Optional display name (profile metadata); null if unset. */
|
||||
fullName: string | null;
|
||||
}
|
||||
|
||||
/** Does this session grant the permission? Central authz check for the SPA. */
|
||||
@@ -80,6 +85,11 @@ export function setLanguagePref(language: Lang): Promise<{ language: Lang }> {
|
||||
return apiFetch("/api/auth/language", { method: "PUT", body: JSON.stringify({ language }) });
|
||||
}
|
||||
|
||||
/** Persist the current user's UI theme preference (restored on next login). */
|
||||
export function setThemePref(theme: Theme): Promise<{ theme: Theme }> {
|
||||
return apiFetch("/api/auth/theme", { method: "PUT", body: JSON.stringify({ theme }) });
|
||||
}
|
||||
|
||||
/** Returns the current user, or null if not authenticated. */
|
||||
export async function fetchMe(): Promise<SessionUser | null> {
|
||||
try {
|
||||
@@ -92,7 +102,14 @@ export async function fetchMe(): Promise<SessionUser | null> {
|
||||
|
||||
// --- User & role management (RBAC) ----------------------------------------
|
||||
|
||||
export interface ManagedUser {
|
||||
/** Optional profile metadata on a managed user (all nullable). */
|
||||
export interface UserProfile {
|
||||
fullName: string | null;
|
||||
phone: string | null;
|
||||
email: string | null;
|
||||
address: string | null;
|
||||
}
|
||||
export interface ManagedUser extends UserProfile {
|
||||
id: string;
|
||||
username: string;
|
||||
roleId: string;
|
||||
@@ -111,10 +128,15 @@ export interface ManagedRole {
|
||||
export function fetchUsers(): Promise<{ users: ManagedUser[] }> {
|
||||
return apiFetch("/api/users");
|
||||
}
|
||||
export function createUser(body: { username: string; password: string; roleId: string }): Promise<ManagedUser> {
|
||||
export function createUser(
|
||||
body: { username: string; password: string; roleId: string } & Partial<UserProfile>,
|
||||
): Promise<ManagedUser> {
|
||||
return apiFetch("/api/users", { method: "POST", body: JSON.stringify(body) });
|
||||
}
|
||||
export function updateUser(id: string, body: { username?: string; roleId?: string }): Promise<ManagedUser> {
|
||||
export function updateUser(
|
||||
id: string,
|
||||
body: { username?: string; roleId?: string } & Partial<UserProfile>,
|
||||
): Promise<ManagedUser> {
|
||||
return apiFetch(`/api/users/${id}`, { method: "PUT", body: JSON.stringify(body) });
|
||||
}
|
||||
export function resetUserPassword(id: string, password: string): Promise<{ ok: boolean }> {
|
||||
@@ -517,6 +539,39 @@ export function recordCashMovement(
|
||||
});
|
||||
}
|
||||
|
||||
/** A completed shift (reconstructed from its signed Z-report). */
|
||||
export interface ShiftSummary {
|
||||
id: string;
|
||||
index: number;
|
||||
operator: string;
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
cashTotalMinor: number;
|
||||
cardTotalMinor: number;
|
||||
currency: string | null;
|
||||
paymentCount: number;
|
||||
openingFloatMinor: number;
|
||||
cashAddedMinor: number;
|
||||
cashRemovedMinor: number;
|
||||
expectedDrawerMinor: number;
|
||||
}
|
||||
|
||||
/** Completed shift history. The server scopes by permission: operators get their
|
||||
* own shifts only (filter args ignored); admins (shift:cash) get all, optionally
|
||||
* filtered by operator + a from/to window over the shift start. `scope` echoes
|
||||
* which the server applied, so the UI can show/hide the filter. */
|
||||
export function fetchShifts(params: { operator?: string; from?: string; to?: string } = {}): Promise<{
|
||||
shifts: ShiftSummary[];
|
||||
scope: "all" | "self";
|
||||
}> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.operator) qs.set("operator", params.operator);
|
||||
if (params.from) qs.set("from", params.from);
|
||||
if (params.to) qs.set("to", params.to);
|
||||
const q = qs.toString();
|
||||
return apiFetch(`/api/shifts${q ? `?${q}` : ""}`);
|
||||
}
|
||||
|
||||
// --- Site config / occupancy ----------------------------------------------
|
||||
|
||||
export interface Occupancy {
|
||||
|
||||
Reference in New Issue
Block a user