fix(backoffice): map overview layouts to the org's real position keys

The overview layout table matched invented keys (`edr_operations_officer`,
`edr_marketing`, …) that only ever existed as IAM roles. The positions
actually configured under the unit use their own keys — `edr_freight_app/opn`,
`ethiopian_gl`, `edr_freight_app/finance` — so most staff fell through to the
executive fallback regardless of desk.

Map every position key in the current org tree, roots and sub-positions
alike, and keep the legacy role-form keys so accounts that model the desks
as roles still resolve. Finance was previously unmapped entirely.

Also drop a stray console.log from resolveOverviewLayout.

Safety (`edr_freight_app/sf_146`) stays unmapped — no such layout exists yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathnael
2026-08-20 08:20:00 +00:00
parent 1f11c3a4ca
commit 6e95c5b8b8
2 changed files with 94 additions and 29 deletions

View File

@@ -4,44 +4,93 @@ import { getPositionKeys } from "@/lib/permissions";
/** One overview composition. Every backoffice user lands on exactly one of these. */
export type OverviewLayoutKey =
| "executive"
| "operations"
| "operation"
| "occ"
| "marketing"
| "marketer"
| "finance"
| "clearance";
export const OVERVIEW_LAYOUT_LABEL: Record<OverviewLayoutKey, string> = {
executive: "Executive dashboard",
operations: "Operations dashboard",
operation: "Operations dashboard",
occ: "Control centre dashboard",
marketing: "Marketing dashboard",
marketer: "Marketing dashboard",
finance: "Finance dashboard",
clearance: "Clearance & logistics dashboard",
};
/**
* Role/position key → layout, in match priority order: a user holding several
* Position/role key → layout, in match priority order: a user holding several
* of these keys gets the first match, so the specific operational view wins
* over the broad executive one. Position keys are matched too because the IAM
* payload models the GL desks as positions (`ethiopian_gl`) on some accounts
* and as roles (`edr_gl_ethiopia`) on others — see `getPositionKeys`.
* over the broad executive one. Roles are matched alongside positions because
* the IAM payload models the GL desks as positions (`ethiopian_gl`) on some
* accounts and as roles (`edr_gl_ethiopia`) on others — see `getPositionKeys`.
*
* The `edr_freight_app/…` keys are the org's real position keys (root desks and
* their sub-positions) as configured under Unit → Departments. They are typed
* by hand in the Add/Edit Department form, so a new sub-position appears here
* only once someone adds it — unmapped keys fall through to `executive`.
*/
const ROLE_LAYOUTS: Array<[key: string, layout: OverviewLayoutKey]> = [
["edr_operations_officer", "operations"],
["truck_machinery_chief", "operations"],
["edr_line_staff", "occ"],
["edr_gl_ethiopia", "clearance"],
["edr_gl_djibouti", "clearance"],
// ── Clearance & logistics: both GL desks, root and sub-positions ──────────
["ethiopian_gl", "clearance"],
["edr_freight_app/gl_003", "clearance"], // Ethiopian GL Chief
["edr_freight_app/off_001", "clearance"], // Ethiopian GL Director
["edr_freight_app/off_0056", "clearance"], // Ethiopian GL Officer
["djibouti_gl", "clearance"],
["edr_marketing", "marketing"],
["edr_finance", "finance"],
["edr_director", "executive"],
["edr_ceo", "executive"],
["edr_org_manager", "executive"],
["edr_freight_app/dj_gl_001", "clearance"], // Djibouti GL Director
["edr_freight_app/dj_gl_002", "clearance"], // Djibouti GL Chief
["edr_freight_app/dj_gl_003", "clearance"], // Djibouti GL Officer
["edr_gl_ethiopia", "clearance"], // legacy role form
["edr_gl_djibouti", "clearance"], // legacy role form
// ── Control centre ───────────────────────────────────────────────────────
["edr_freight_app/occ_001", "occ"], // OCC
["edr_freight_app/occ_005", "occ"], // OCC Director
["edr_line_staff", "occ"], // legacy role form
// ── Operations: operations desk, track & machinery, rolling stock ─────────
["edr_freight_app/opn", "operation"], // Operation
["edr_freight_app/opcf", "operation"], // Operation Chief
["edr_freight_app/opdr", "operation"], // Operation Director
["edr_freight_app/opco", "operation"], // Operation Officer
["edr_freight_app/opp_005", "operation"], // Operation Dispatcher
["edr_freight_app/opp_0067", "operation"], // Gelan Operation Director
["edr_freight_app/track_001", "operation"], // Track And Machinery
["edr_freight_app/ttk_001", "operation"], // Track Director
["edr_freight_app/tto_001", "operation"], // Track Operator
["edr_freight_app/rool_001", "operation"], // Rolling Stock
["edr_freight_app/rl_003", "operation"], // Rolling Stock Director
["edr_freight_app/rl_009", "operation"], // Rolling Stock Team Lead
["edr_freight_app/rl_0090", "operation"], // Rolling Stock Dispatcher
["operation", "operation"],
["operations_chief", "operation"],
["dispatcher", "operation"],
["truck_machinery_chief", "operation"],
["edr_operations_officer", "operation"], // legacy role form
// ── Marketing ────────────────────────────────────────────────────────────
["edr_freight_app/edr_test_org_0022", "marketer"], // Commercial Marketing
["edr_freight_app/edr_test_org_00567", "marketer"], // Marketing Director
["edr_freight_app/edr_test_org_0054", "marketer"], // Marketing Chief
["edr_freight_app/edr_test_org_0013", "marketer"], // Marketing Officer
["marketer", "marketer"],
["edr_marketing", "marketer"], // legacy role form
// ── Finance ──────────────────────────────────────────────────────────────
["edr_freight_app/finance", "finance"],
["edr_finance", "finance"], // legacy role form
// ── Executive: org-wide desks with no operational queue of their own ──────
["ceo", "executive"],
["director", "executive"],
["chief", "executive"],
["edr_ceo", "executive"], // legacy role form
["edr_director", "executive"], // legacy role form
["edr_org_manager", "executive"], // legacy role form
];
/** Unmapped roles (superadmin, IAM admins, new roles) keep the executive layout. */
/** Unmapped keys (superadmin, IAM admins, Safety, new positions) keep the executive layout. */
export function resolveOverviewLayout(
user: AuthUser | null | undefined,
): OverviewLayoutKey {

View File

@@ -24,14 +24,21 @@ import { useOverview } from "@/hooks/useOverview";
import type { OverviewRange } from "@/types/overview";
import "@/components/overview/summary/overview-summary.css";
const RANGE_LABEL: Record<OverviewRange, string> = { "7d": "7d", "30d": "30d", "90d": "90d" };
const RANGE_LABEL: Record<OverviewRange, string> = {
"7d": "7d",
"30d": "30d",
"90d": "90d",
};
/** Which composition each role sees below the hero. */
const LAYOUTS: Record<OverviewLayoutKey, (props: RoleOverviewProps) => ReactElement> = {
const LAYOUTS: Record<
OverviewLayoutKey,
(props: RoleOverviewProps) => ReactElement
> = {
executive: ExecutiveOverview,
operations: OperationsOverview,
operation: OperationsOverview,
occ: OccOverview,
marketing: MarketingOverview,
marketer: MarketingOverview,
finance: FinanceOverview,
clearance: ClearanceOverview,
};
@@ -51,15 +58,17 @@ const OverviewPage = () => {
const [range, setRange] = useState<OverviewRange>("30d");
const queryClient = useQueryClient();
const { user } = useAuth();
const { data, isLoading, isError, error, refetch, isFetching } = useOverview(range);
const { data, isLoading, isError, error, refetch, isFetching } =
useOverview(range);
// Hero, range control and headline KPIs are role-neutral; everything below
// them is chosen by role key.
const layoutKey = resolveOverviewLayout(user);
const RoleLayout = LAYOUTS[layoutKey];
const RoleLayout = layoutKey ? LAYOUTS[layoutKey] : null;
const accessDenied =
(error as { response?: { status?: number } } | null)?.response?.status === 403;
(error as { response?: { status?: number } } | null)?.response?.status ===
403;
const handleRefresh = () => {
void refetch();
@@ -79,7 +88,9 @@ const OverviewPage = () => {
label={OVERVIEW_LAYOUT_LABEL[layoutKey]}
/>
{data ? (
<div style={{ marginTop: -52, paddingInline: 20, position: "relative" }}>
<div
style={{ marginTop: -52, paddingInline: 20, position: "relative" }}
>
<OverviewHeroKpis
kpis={data.kpis}
current={data.current}
@@ -100,7 +111,12 @@ const OverviewPage = () => {
>
<Stack gap="sm" align="flex-start">
<span>Check your connection and try again.</span>
<Button size="xs" variant="light" color="red" onClick={() => void refetch()}>
<Button
size="xs"
variant="light"
color="red"
onClick={() => void refetch()}
>
Retry
</Button>
</Stack>
@@ -117,7 +133,7 @@ const OverviewPage = () => {
<Stack mt="lg">
<OverviewSkeleton />
</Stack>
) : data ? (
) : data && RoleLayout ? (
<RoleLayout data={data} range={range} />
) : null}
</PageContainer>