diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx
index f12ba9c6c..e8879ba0e 100644
--- a/apps/edr-freight-web/backoffice/src/App.tsx
+++ b/apps/edr-freight-web/backoffice/src/App.tsx
@@ -527,6 +527,7 @@ const App = () => {
}
/>
} />
+ } />
} />
(v == null ? "" : String(v));
const orderOf = (n: CargoNode): number => Number(n.displayOrder ?? 0);
-/** Fields for the create/edit form. Parent-group select is added contextually. */
-const PARENT_FORM_FIELDS: FormFieldDef[] = [
+/** Create/edit form fields. Parent is set from the current page, never picked. */
+const FORM_FIELDS: FormFieldDef[] = [
{ name: "cargoTypeName", label: "Cargo type name", type: "text", required: true },
{ name: "showFreeTextBox", label: "Show free text box", type: "boolean" },
{ name: "requiresDirectorApproval", label: "Requires director approval", type: "boolean" },
{ name: "isActive", label: "Active", type: "boolean" },
];
-type FormMode =
- | { kind: "create-parent" }
- | { kind: "create-child"; parent: CargoNode }
- | { kind: "edit"; record: CargoNode };
+type FormMode = { kind: "create" } | { kind: "edit"; record: CargoNode };
const CargoTypesPage = () => {
const { user } = useAuth();
+ const navigate = useNavigate();
+ const { id: currentId } = useParams<{ id: string }>();
const config = getRuleEngineResource(CARGO_SLUG);
const canView = canAccessRuleEngineResource(user, CARGO_SLUG, "view");
const canManage = canAccessRuleEngineResource(user, CARGO_SLUG, "manage");
- // Pull the whole set once and build the tree on the client — cargo types are a
- // small, slow-changing config set, so a single fetch keeps the UX instant.
+ // One fetch of the whole (small) set; the tree, ancestry and each level are
+ // derived client-side so drilling between levels is instant.
const { data, isLoading, isError } = useRuleEngineList(CARGO_SLUG, {
page: 1,
pageSize: 500,
@@ -91,111 +90,72 @@ const CargoTypesPage = () => {
const { create, update, remove } = useRuleEngineMutations(CARGO_SLUG);
const [search, setSearch] = useState("");
- const [expanded, setExpanded] = useState>(new Set());
const [formMode, setFormMode] = useState(null);
const [deleteTarget, setDeleteTarget] = useState(null);
const all = (data?.data ?? []) as CargoNode[];
- // Group children under their parents; anything without a (known) parent is a root.
- const { roots, childrenByParent } = useMemo(() => {
- const byId = new Map(all.map((n) => [n.id, n]));
- const childrenByParent = new Map();
- const roots: CargoNode[] = [];
+ const { byId, childrenOf } = useMemo(() => {
+ const byId = new Map(all.map((n) => [n.id, n]));
+ const childrenOf = new Map();
for (const node of all) {
- const parentId = node.parentGroupId ?? null;
- if (parentId && byId.has(parentId)) {
- const list = childrenByParent.get(parentId) ?? [];
- list.push(node);
- childrenByParent.set(parentId, list);
- } else {
- roots.push(node);
- }
+ const parentId = node.parentGroupId && byId.has(node.parentGroupId) ? node.parentGroupId : "";
+ const key = parentId || "__root__";
+ const list = childrenOf.get(key) ?? [];
+ list.push(node);
+ childrenOf.set(key, list);
}
- for (const list of childrenByParent.values()) {
- list.sort((a, b) => orderOf(a) - orderOf(b) || str(a.cargoTypeName).localeCompare(str(b.cargoTypeName)));
+ for (const list of childrenOf.values()) {
+ list.sort(
+ (a, b) =>
+ orderOf(a) - orderOf(b) ||
+ str(a.cargoTypeName).localeCompare(str(b.cargoTypeName)),
+ );
}
- roots.sort((a, b) => orderOf(a) - orderOf(b) || str(a.cargoTypeName).localeCompare(str(b.cargoTypeName)));
- return { roots, childrenByParent };
+ return { byId, childrenOf };
}, [all]);
- // Search matches a parent directly, or surfaces a parent because a child matches.
+ // Current node (null at root) and its ancestor chain for the breadcrumb.
+ const current = currentId ? byId.get(currentId) ?? null : null;
+ const ancestors = useMemo(() => {
+ const chain: CargoNode[] = [];
+ let node = current;
+ const seen = new Set();
+ while (node && !seen.has(node.id)) {
+ chain.unshift(node);
+ seen.add(node.id);
+ node = node.parentGroupId ? byId.get(node.parentGroupId) ?? null : null;
+ }
+ return chain;
+ }, [current, byId]);
+
+ const levelKey = current ? current.id : "__root__";
+ const levelNodes = childrenOf.get(levelKey) ?? [];
+
const term = search.trim().toLowerCase();
const matches = (n: CargoNode) =>
!term ||
str(n.cargoTypeName).toLowerCase().includes(term) ||
str(n.code).toLowerCase().includes(term);
-
- const visibleRoots = useMemo(() => {
- if (!term) return roots;
- return roots.filter(
- (p) => matches(p) || (childrenByParent.get(p.id) ?? []).some(matches),
- );
- }, [roots, childrenByParent, term]);
+ const visibleNodes = useMemo(
+ () => (term ? levelNodes.filter(matches) : levelNodes),
+ [levelNodes, term],
+ );
if (!config) return ;
if (!canView) return ;
+ // A bad/stale :id (after data loads) → fall back to the root list.
+ if (!isLoading && currentId && !current) return ;
- const toggle = (id: string) =>
- setExpanded((prev) => {
- const next = new Set(prev);
- next.has(id) ? next.delete(id) : next.add(id);
- return next;
- });
-
- const parentCount = roots.length;
- const childCount = all.length - roots.length;
-
- // ── form wiring ──────────────────────────────────────────────────────────
- const formFields: FormFieldDef[] =
- formMode?.kind === "edit" && !formMode.record.parentGroupId
- ? // editing a parent → allow re-parenting via a select of OTHER roots
- [
- PARENT_FORM_FIELDS[0],
- {
- name: "parentGroupId",
- label: "Parent group",
- type: "select",
- optional: true,
- placeholder: "Top-level (no parent)",
- options: [
- { label: "None (top level)", value: RULE_ENGINE_SELECT_NONE },
- ...roots
- .filter((r) => r.id !== formMode.record.id)
- .map((r) => ({ label: str(r.cargoTypeName), value: r.id })),
- ],
- },
- ...PARENT_FORM_FIELDS.slice(1),
- ]
- : // creating a parent, creating a child, or editing a child → no parent picker
- PARENT_FORM_FIELDS;
-
- const initialRecord: RuleEngineRecord | null =
- formMode?.kind === "edit" ? formMode.record : null;
-
- const formTitle =
- formMode?.kind === "create-parent"
- ? "Add cargo type"
- : formMode?.kind === "create-child"
- ? `Add cargo under “${str(formMode.parent.cargoTypeName)}”`
- : formMode?.kind === "edit"
- ? `Edit ${str(formMode.record.cargoTypeName)}`
- : "";
-
- const formDescription =
- formMode?.kind === "create-parent"
- ? "Create a top-level cargo category. You can add cargo types under it afterwards."
- : formMode?.kind === "create-child"
- ? "Create a cargo type inside this category."
- : "Update this cargo type.";
+ const atRoot = !current;
+ const countAtRoot = (childrenOf.get("__root__") ?? []).length;
const handleSubmit = (values: Record) => {
- // Force the parent context for child creates; the form has no parent picker.
const payload: Record = { ...values };
- if (formMode?.kind === "create-child") {
- payload.parentGroupId = formMode.parent.id;
+ // Add always attaches to the page we're on; edit keeps the node's parent.
+ if (formMode?.kind === "create" && current) {
+ payload.parentGroupId = current.id;
}
-
const done = () => setFormMode(null);
if (formMode?.kind === "edit") {
update.mutate({ id: formMode.record.id, payload }, { onSuccess: done });
@@ -204,6 +164,8 @@ const CargoTypesPage = () => {
}
};
+ const addLabel = atRoot ? "Add category" : "Add cargo type";
+
return (
{/* ── Header ─────────────────────────────────────────────── */}
@@ -213,26 +175,61 @@ const CargoTypesPage = () => {
withBorder
style={{ background: "white", boxShadow: "0 1px 3px rgba(0,0,0,0.05)" }}
>
-
-
-
-
-
-
-
+ {/* Breadcrumb */}
+ }
+ mb="md"
+ >
+ navigate(BASE_PATH)}>
+
+
+
Cargo Types
-
- Organise freight cargo into categories and the cargo types within them.
-
-
-
- {parentCount} categor{parentCount === 1 ? "y" : "ies"}
-
-
- {childCount} cargo type{childCount === 1 ? "" : "s"}
-
+
+
+ {ancestors.map((node, i) => {
+ const isLast = i === ancestors.length - 1;
+ return (
+ !isLast && navigate(`${BASE_PATH}/${node.id}`)}
+ style={{ cursor: isLast ? "default" : "pointer" }}
+ >
+
+ {str(node.cargoTypeName) || "Untitled"}
+
+
+ );
+ })}
+
+
+
+
+
+ {atRoot ? : }
+
+
+
+
+ {atRoot ? "Cargo Types" : str(current?.cargoTypeName) || "Untitled"}
+
+ {!atRoot && current?.code ? (
+
+ {str(current.code)}
+
+ ) : null}
+ {!atRoot && current?.isActive === false ? (
+
+ Inactive
+
+ ) : null}
+
+ {atRoot
+ ? `${countAtRoot} top-level categor${countAtRoot === 1 ? "y" : "ies"} — click one to see what's inside`
+ : `${levelNodes.length} cargo type${levelNodes.length === 1 ? "" : "s"} directly under this category`}
+
@@ -240,29 +237,33 @@ const CargoTypesPage = () => {
setSearch(e.currentTarget.value)}
- placeholder="Search categories or cargo…"
+ placeholder="Search this level…"
leftSection={}
- w={260}
+ w={240}
/>
{canManage && (
}
- onClick={() => setFormMode({ kind: "create-parent" })}
+ onClick={() => setFormMode({ kind: "create" })}
>
- Add category
+ {addLabel}
)}
- {/* ── Tree ───────────────────────────────────────────────── */}
+ {/* ── Level list ─────────────────────────────────────────── */}
{isLoading ? (
@@ -272,67 +273,43 @@ const CargoTypesPage = () => {
Failed to load cargo types.
- ) : visibleRoots.length === 0 ? (
+ ) : visibleNodes.length === 0 ? (
- {term ? "No cargo types match your search" : "No cargo categories yet"}
+ {term
+ ? "Nothing matches your search"
+ : atRoot
+ ? "No cargo categories yet"
+ : `No cargo types under “${str(current?.cargoTypeName)}” yet`}
{!term && canManage && (
}
- onClick={() => setFormMode({ kind: "create-parent" })}
+ onClick={() => setFormMode({ kind: "create" })}
>
- Add your first category
+ {atRoot ? "Add your first category" : "Add the first cargo type"}
)}
) : (
- {visibleRoots.map((parent, i) => {
- const kids = childrenByParent.get(parent.id) ?? [];
- const isOpen = expanded.has(parent.id) || Boolean(term);
- const shownKids = term ? kids.filter(matches) : kids;
- return (
- 0}
- canManage={canManage}
- onToggle={() => toggle(parent.id)}
- onEdit={() => setFormMode({ kind: "edit", record: parent })}
- onDelete={() => setDeleteTarget(parent)}
- onAddChild={() => {
- setExpanded((prev) => new Set(prev).add(parent.id));
- setFormMode({ kind: "create-child", parent });
- }}
- >
- {shownKids.length === 0 ? (
-
- No cargo types in this category yet.
-
- ) : (
-
- {shownKids.map((child) => (
- setFormMode({ kind: "edit", record: child })}
- onDelete={() => setDeleteTarget(child)}
- />
- ))}
-
- )}
-
- );
- })}
+ {visibleNodes.map((node, i) => (
+ 0}
+ canManage={canManage}
+ onOpen={() => navigate(`${BASE_PATH}/${node.id}`)}
+ onEdit={() => setFormMode({ kind: "edit", record: node })}
+ onDelete={() => setDeleteTarget(node)}
+ />
+ ))}
)}
@@ -343,10 +320,22 @@ const CargoTypesPage = () => {
onOpenChange={(open) => {
if (!open) setFormMode(null);
}}
- title={formTitle}
- description={formDescription}
- fields={formFields}
- initialRecord={initialRecord}
+ title={
+ formMode?.kind === "edit"
+ ? `Edit ${str(formMode.record.cargoTypeName)}`
+ : atRoot
+ ? "Add category"
+ : `Add cargo under “${str(current?.cargoTypeName)}”`
+ }
+ description={
+ formMode?.kind === "edit"
+ ? "Update this cargo type."
+ : atRoot
+ ? "Create a top-level cargo category."
+ : "Create a cargo type inside this category. It's attached here automatically."
+ }
+ fields={FORM_FIELDS}
+ initialRecord={formMode?.kind === "edit" ? formMode.record : null}
isSubmitting={create.isPending || update.isPending}
onSubmit={handleSubmit}
/>
@@ -361,13 +350,13 @@ const CargoTypesPage = () => {
>
- {deleteTarget && (childrenByParent.get(deleteTarget.id)?.length ?? 0) > 0 ? (
+ {deleteTarget && (childrenOf.get(deleteTarget.id)?.length ?? 0) > 0 ? (
<>
{str(deleteTarget?.cargoTypeName)}
{" "}
- has {childrenByParent.get(deleteTarget!.id)?.length} cargo type(s) under it.
- Deleting it leaves them without a category. Continue?
+ has {childrenOf.get(deleteTarget!.id)?.length} cargo type(s) under it. Deleting it
+ leaves them without a category. Continue?
>
) : (
<>
@@ -388,9 +377,7 @@ const CargoTypesPage = () => {
loading={remove.isPending}
onClick={() => {
if (!deleteTarget) return;
- remove.mutate(deleteTarget.id, {
- onSuccess: () => setDeleteTarget(null),
- });
+ remove.mutate(deleteTarget.id, { onSuccess: () => setDeleteTarget(null) });
}}
>
Delete
@@ -402,197 +389,122 @@ const CargoTypesPage = () => {
);
};
-// ── Parent (category) row ──────────────────────────────────────────────────
-interface ParentRowProps {
- parent: CargoNode;
+// ── A single cargo row — drills into its own page on click ──────────────────
+interface CargoRowProps {
+ node: CargoNode;
childCount: number;
- open: boolean;
topBorder: boolean;
canManage: boolean;
- onToggle: () => void;
+ onOpen: () => void;
onEdit: () => void;
onDelete: () => void;
- onAddChild: () => void;
- children: React.ReactNode;
}
-function ParentRow({
- parent,
+function CargoRow({
+ node,
childCount,
- open,
topBorder,
canManage,
- onToggle,
+ onOpen,
onEdit,
onDelete,
- onAddChild,
- children,
-}: ParentRowProps) {
- const inactive = parent.isActive === false;
- return (
-
-
-
-
-
-
-
-
-
-
-
- {str(parent.cargoTypeName) || "Untitled"}
-
- {parent.code ? (
-
- {str(parent.code)}
-
- ) : null}
- {inactive ? (
-
- Inactive
-
- ) : null}
-
-
- {childCount} cargo type{childCount === 1 ? "" : "s"}
-
-
-
-
-
-
- {canManage && (
-
- }
- onClick={onAddChild}
- >
- Add
-
-
- )}
- {canManage && (
- <>
-
-
-
-
-
-
- >
- )}
-
-
-
-
- {children}
-
-
- );
-}
-
-// ── Child (cargo type) row ─────────────────────────────────────────────────
-interface ChildRowProps {
- child: CargoNode;
- canManage: boolean;
- onEdit: () => void;
- onDelete: () => void;
-}
-
-function ChildRow({ child, canManage, onEdit, onDelete }: ChildRowProps) {
- const inactive = child.isActive === false;
+}: CargoRowProps) {
+ const inactive = node.isActive === false;
+ const hasChildren = childCount > 0;
return (
{
+ e.currentTarget.style.background = "var(--mantine-color-teal-0)";
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.background = "";
+ }}
>
-
-
-
- {str(child.cargoTypeName) || "Untitled"}
-
- {child.code ? (
-
- {str(child.code)}
-
- ) : null}
- {child.requiresDirectorApproval ? (
-
- }
- >
- Approval
-
-
- ) : null}
- {child.showFreeTextBox ? (
-
- }
- >
- Free text
-
-
- ) : null}
- {inactive ? (
-
- Inactive
-
- ) : null}
-
-
- {canManage && (
-
-
-
-
-
-
-
+
+
+
+ {hasChildren ? : }
+
+
+
+
+ {str(node.cargoTypeName) || "Untitled"}
+
+ {node.code ? (
+
+ {str(node.code)}
+
+ ) : null}
+ {node.requiresDirectorApproval ? (
+
+ }
+ >
+ Approval
+
+
+ ) : null}
+ {node.showFreeTextBox ? (
+
+ }
+ >
+ Free text
+
+
+ ) : null}
+ {inactive ? (
+
+ Inactive
+
+ ) : null}
+
+
+ {hasChildren
+ ? `${childCount} cargo type${childCount === 1 ? "" : "s"} inside`
+ : "No cargo types inside yet — open to add"}
+
+
- )}
+
+
+
+ {canManage && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
);
}