Files
PacketStation/frontend/src/auth/roles.ts
T

138 lines
4.9 KiB
TypeScript
Raw Normal View History

// Role strings, the static role-to-screen map and the labels.
// The server decides access on every request. This file only shapes the screens.
import type { Department } from "./types";
/** Screens in the order the role switcher shows them (the old build's order). */
export const SCREENS = ["clerical", "warehouse", "supervisor", "stall", "qc", "pm", "admin"] as const;
export type Screen = (typeof SCREENS)[number];
export const SCREEN_LABELS: Record<Screen, string> = {
clerical: "Clerical",
warehouse: "Warehouse",
supervisor: "Supervisor",
stall: "Weld Stall",
qc: "Q.C.",
pm: "Project Mgmt",
admin: "Admin",
};
/** The phase in which each placeholder screen gets its real content. */
export const SCREEN_PHASE: Record<Exclude<Screen, "admin">, number> = {
clerical: 2,
warehouse: 4,
supervisor: 5,
stall: 5,
qc: 6,
pm: 8,
};
export function isScreen(s: string | undefined): s is Screen {
return s !== undefined && (SCREENS as readonly string[]).includes(s);
}
export type ShopLevel = "worker" | "lead" | "supervisor";
export type ParsedRole =
| { shop: false; kind: string }
| { shop: true; dept: string; level: ShopLevel };
const SHOP_LEVELS: readonly ShopLevel[] = ["worker", "lead", "supervisor"];
/** "shop:03:supervisor" -> shop role; anything else is an office role kind. */
export function parseRole(role: string): ParsedRole {
const m = /^shop:([^:]+):(worker|lead|supervisor)$/.exec(role);
if (m && m[1] !== undefined && m[2] !== undefined) {
return { shop: true, dept: m[1], level: m[2] as ShopLevel };
}
return { shop: false, kind: role };
}
/** Turns a role kind and department ("shop_lead", "03") into the role string ("shop:03:lead"). */
export function roleFromKind(kind: string, dept: string | null): string {
if (kind.includes(":")) return kind;
const m = /^shop_(worker|lead|supervisor)$/.exec(kind);
if (m && m[1] && dept) return `shop:${dept}:${m[1]}`;
return kind;
}
/** The static map. Same rule as the server's screens_for(roles). */
export function screensFor(roles: readonly string[]): Screen[] {
const out = new Set<Screen>();
for (const r of roles) {
const p = parseRole(r);
if (p.shop) {
out.add("stall");
if (p.level !== "worker") out.add("supervisor");
continue;
}
if (p.kind === "clerical" || p.kind === "clerical_supervisor") out.add("clerical");
else if (p.kind.startsWith("warehouse")) out.add("warehouse");
else if (p.kind.startsWith("qc")) out.add("qc");
else if (p.kind === "pm") out.add("pm");
else if (p.kind === "admin") SCREENS.forEach((s) => out.add(s));
}
return SCREENS.filter((s) => out.has(s));
}
/** Known screens from a server list, in switcher order. */
export function orderScreens(list: readonly string[]): Screen[] {
return SCREENS.filter((s) => list.includes(s));
}
/** The screen a user lands on: the first non-admin screen, else admin, else null. */
export function firstScreen(list: readonly string[]): Screen | null {
const ordered = orderScreens(list);
return ordered.find((s) => s !== "admin") ?? ordered[0] ?? null;
}
export function homePath(list: readonly string[]): string {
const s = firstScreen(list);
return s ? `/${s}` : "/account";
}
const OFFICE_LABELS: Record<string, string> = {
admin: "Admin",
clerical: "Clerical",
clerical_supervisor: "Clerical supervisor",
warehouse: "Warehouse",
warehouse_supervisor: "Warehouse supervisor",
qc: "Q.C.",
qc_supervisor: "Q.C. supervisor",
pm: "Project Management",
};
const LEVEL_LABELS: Record<ShopLevel, string> = { worker: "Worker", lead: "Lead", supervisor: "Supervisor" };
/** "shop:03:supervisor" -> "03 Weld Parts · Supervisor". Office roles get their plain name. */
export function roleLabel(role: string, departments: readonly Department[] = []): string {
const p = parseRole(role);
if (!p.shop) return OFFICE_LABELS[p.kind] ?? p.kind;
const dept = departments.find((d) => d.code === p.dept);
const where = dept ? `${p.dept} ${dept.name}` : p.dept;
return `${where} · ${LEVEL_LABELS[p.level]}`;
}
/** The .rchip variant for a role: supervisors pink, leads green, admin amber. */
export function roleChipClass(role: string): string {
const p = parseRole(role);
if (p.shop) return p.level === "worker" ? "rchip" : `rchip ${p.level}`;
if (p.kind === "admin") return "rchip admin";
if (p.kind.endsWith("_supervisor")) return "rchip supervisor";
return "rchip";
}
/** Office roles first in a fixed order, then shop roles by department and level. */
export function sortRoles(roles: readonly string[]): string[] {
const office = Object.keys(OFFICE_LABELS);
const rank = (r: string): [number, string, number] => {
const p = parseRole(r);
if (!p.shop) return [0, "", office.includes(p.kind) ? office.indexOf(p.kind) : office.length];
return [1, p.dept, SHOP_LEVELS.indexOf(p.level)];
};
return [...roles].sort((a, b) => {
const [a0, a1, a2] = rank(a);
const [b0, b1, b2] = rank(b);
return a0 - b0 || a1.localeCompare(b1) || a2 - b2;
});
}