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:
@@ -23,10 +23,26 @@ interface LanguageBody {
|
||||
language: Lang;
|
||||
}
|
||||
|
||||
const THEMES = ["dark", "light"] as const;
|
||||
type Theme = (typeof THEMES)[number];
|
||||
interface ThemeBody {
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
/** The session shape the SPA bootstraps from: identity + role + its permission
|
||||
* list (so the UI can gate nav/routes) + language. Role NAME is for display; the
|
||||
* permissions are the source of truth. */
|
||||
function sessionView(db: Db, user: { id: string; username: string; roleId: string; language: string }) {
|
||||
function sessionView(
|
||||
db: Db,
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
roleId: string;
|
||||
language: string;
|
||||
theme: string;
|
||||
fullName?: string | null;
|
||||
},
|
||||
) {
|
||||
const role = db.select().from(roles).where(eq(roles.id, user.roleId)).get();
|
||||
const permissions = [...permissionsFor(user.roleId)];
|
||||
return {
|
||||
@@ -36,6 +52,8 @@ function sessionView(db: Db, user: { id: string; username: string; roleId: strin
|
||||
roleName: role?.name ?? user.roleId,
|
||||
permissions,
|
||||
language: user.language,
|
||||
theme: user.theme,
|
||||
fullName: user.fullName ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,4 +124,19 @@ export async function authRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
||||
return { language };
|
||||
},
|
||||
);
|
||||
|
||||
// Change MY own UI theme preference (any signed-in user). Persisted to the users
|
||||
// row like `language`, so it's restored on the next login from any booth.
|
||||
app.put<{ Body: ThemeBody }>(
|
||||
"/api/auth/theme",
|
||||
{ preHandler: requireAuth },
|
||||
async (req, reply) => {
|
||||
const theme = req.body?.theme;
|
||||
if (!theme || !THEMES.includes(theme)) {
|
||||
return reply.code(400).send({ error: `theme must be one of: ${THEMES.join(", ")}` });
|
||||
}
|
||||
await db.update(users).set({ theme }).where(eq(users.id, req.user.sub)).run();
|
||||
return { theme };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user