138 lines
4.9 KiB
TypeScript
138 lines
4.9 KiB
TypeScript
// 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;
|
||
|
|
});
|
||
|
|
}
|