feat(cargo-types): add route for cargo types detail view and enhance CargoTypesPage with breadcrumbs and improved navigation

This commit is contained in:
Marshal
2026-06-18 20:49:46 +00:00
parent aa105a5e76
commit ed114b1999
2 changed files with 258 additions and 345 deletions

View File

@@ -527,6 +527,7 @@ const App = () => {
}
/>
<Route path="configuration/cargo-types" element={<CargoTypesPage />} />
<Route path="configuration/cargo-types/:id" element={<CargoTypesPage />} />
<Route path="configuration/:resource" element={<RuleEngineResourcePage />} />
<Route

View File

@@ -1,11 +1,11 @@
import { useMemo, useState } from "react";
import { Navigate } from "react-router-dom";
import { Navigate, useNavigate, useParams } from "react-router-dom";
import {
Badge,
Box,
Breadcrumbs,
Button,
Card,
Collapse,
Group,
Loader,
Modal,
@@ -19,8 +19,9 @@ import {
import {
Boxes,
ChevronRight,
CornerDownRight,
FileText,
Home,
Layers,
Package,
Pencil,
Plus,
@@ -33,7 +34,6 @@ import { useAuth } from "@/auth/useAuth";
import { canAccessRuleEngineResource } from "@/lib/permissions";
import RuleEngineFormDialog from "@/components/ruleEngine/RuleEngineFormDialog";
import {
RULE_ENGINE_SELECT_NONE,
getRuleEngineResource,
type FormFieldDef,
} from "@/pages/ruleEngine/config/resources";
@@ -44,8 +44,8 @@ import {
import type { RuleEngineRecord } from "@/types/rule-engine";
const CARGO_SLUG = "cargo-types";
const BASE_PATH = "/dashboard/configuration/cargo-types";
/** A cargo type with its (already-resolved) child cargo types. */
interface CargoNode extends RuleEngineRecord {
cargoTypeName?: string;
code?: string;
@@ -59,28 +59,27 @@ interface CargoNode extends RuleEngineRecord {
const str = (v: unknown): string => (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<Set<string>>(new Set());
const [formMode, setFormMode] = useState<FormMode | null>(null);
const [deleteTarget, setDeleteTarget] = useState<CargoNode | null>(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<string, CargoNode[]>();
const roots: CargoNode[] = [];
const { byId, childrenOf } = useMemo(() => {
const byId = new Map<string, CargoNode>(all.map((n) => [n.id, n]));
const childrenOf = new Map<string, CargoNode[]>();
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<string>();
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 <Navigate to="/dashboard/overview" replace />;
if (!canView) return <Navigate to="/dashboard/overview" replace />;
// A bad/stale :id (after data loads) → fall back to the root list.
if (!isLoading && currentId && !current) return <Navigate to={BASE_PATH} replace />;
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<string, unknown>) => {
// Force the parent context for child creates; the form has no parent picker.
const payload: Record<string, unknown> = { ...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 (
<Stack gap="lg">
{/* ── Header ─────────────────────────────────────────────── */}
@@ -213,26 +175,61 @@ const CargoTypesPage = () => {
withBorder
style={{ background: "white", boxShadow: "0 1px 3px rgba(0,0,0,0.05)" }}
>
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
<Group gap="md" wrap="nowrap">
<ThemeIcon size={48} radius="md" variant="light" color="teal">
<Boxes size={26} />
</ThemeIcon>
<Box>
<Text fw={800} fz={22} c="dark.8">
{/* Breadcrumb */}
<Breadcrumbs
separator={<ChevronRight size={14} style={{ color: "var(--mantine-color-gray-5)" }} />}
mb="md"
>
<UnstyledButton onClick={() => navigate(BASE_PATH)}>
<Group gap={5} wrap="nowrap">
<Home size={14} style={{ color: "var(--mantine-color-teal-7)" }} />
<Text fz={13} fw={600} c={atRoot ? "dark.7" : "teal.7"}>
Cargo Types
</Text>
<Text fz={13} c="dimmed" mt={2}>
Organise freight cargo into categories and the cargo types within them.
</Text>
<Group gap="xs" mt={8}>
<Badge variant="light" color="teal" radius="sm">
{parentCount} categor{parentCount === 1 ? "y" : "ies"}
</Badge>
<Badge variant="light" color="gray" radius="sm">
{childCount} cargo type{childCount === 1 ? "" : "s"}
</Badge>
</Group>
</UnstyledButton>
{ancestors.map((node, i) => {
const isLast = i === ancestors.length - 1;
return (
<UnstyledButton
key={node.id}
onClick={() => !isLast && navigate(`${BASE_PATH}/${node.id}`)}
style={{ cursor: isLast ? "default" : "pointer" }}
>
<Text fz={13} fw={isLast ? 700 : 600} c={isLast ? "dark.7" : "teal.7"} truncate maw={220}>
{str(node.cargoTypeName) || "Untitled"}
</Text>
</UnstyledButton>
);
})}
</Breadcrumbs>
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
<Group gap="md" wrap="nowrap" style={{ minWidth: 0 }}>
<ThemeIcon size={48} radius="md" variant="light" color="teal">
{atRoot ? <Boxes size={26} /> : <Layers size={26} />}
</ThemeIcon>
<Box style={{ minWidth: 0 }}>
<Group gap={8} wrap="nowrap">
<Text fw={800} fz={22} c="dark.8" truncate>
{atRoot ? "Cargo Types" : str(current?.cargoTypeName) || "Untitled"}
</Text>
{!atRoot && current?.code ? (
<Badge variant="default" radius="sm">
{str(current.code)}
</Badge>
) : null}
{!atRoot && current?.isActive === false ? (
<Badge variant="light" color="gray" radius="sm">
Inactive
</Badge>
) : null}
</Group>
<Text fz={13} c="dimmed" mt={2}>
{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`}
</Text>
</Box>
</Group>
@@ -240,29 +237,33 @@ const CargoTypesPage = () => {
<TextInput
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
placeholder="Search categories or cargo…"
placeholder="Search this level…"
leftSection={<Search size={16} />}
w={260}
w={240}
/>
{canManage && (
<Button
color="teal"
leftSection={<Plus size={16} />}
onClick={() => setFormMode({ kind: "create-parent" })}
onClick={() => setFormMode({ kind: "create" })}
>
Add category
{addLabel}
</Button>
)}
</Group>
</Group>
</Card>
{/* ── Tree ───────────────────────────────────────────────── */}
{/* ── Level list ─────────────────────────────────────────── */}
<Card
p={0}
radius="lg"
withBorder
style={{ background: "white", boxShadow: "0 1px 3px rgba(0,0,0,0.05)", overflow: "hidden" }}
style={{
background: "white",
boxShadow: "0 1px 3px rgba(0,0,0,0.05)",
overflow: "hidden",
}}
>
{isLoading ? (
<Group justify="center" p="xl">
@@ -272,67 +273,43 @@ const CargoTypesPage = () => {
<Text p="xl" c="red" ta="center">
Failed to load cargo types.
</Text>
) : visibleRoots.length === 0 ? (
) : visibleNodes.length === 0 ? (
<Stack align="center" gap="sm" py={56}>
<ThemeIcon size={52} radius="xl" variant="light" color="gray">
<Package size={26} />
</ThemeIcon>
<Text fw={600} c="dark.6">
{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`}
</Text>
{!term && canManage && (
<Button
variant="light"
color="teal"
leftSection={<Plus size={16} />}
onClick={() => setFormMode({ kind: "create-parent" })}
onClick={() => setFormMode({ kind: "create" })}
>
Add your first category
{atRoot ? "Add your first category" : "Add the first cargo type"}
</Button>
)}
</Stack>
) : (
<Stack gap={0}>
{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 (
<ParentRow
key={parent.id}
parent={parent}
childCount={kids.length}
open={isOpen}
topBorder={i > 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 ? (
<Text fz={13} c="dimmed" pl={56} py="sm">
No cargo types in this category yet.
</Text>
) : (
<Stack gap={0}>
{shownKids.map((child) => (
<ChildRow
key={child.id}
child={child}
canManage={canManage}
onEdit={() => setFormMode({ kind: "edit", record: child })}
onDelete={() => setDeleteTarget(child)}
/>
))}
</Stack>
)}
</ParentRow>
);
})}
{visibleNodes.map((node, i) => (
<CargoRow
key={node.id}
node={node}
childCount={(childrenOf.get(node.id) ?? []).length}
topBorder={i > 0}
canManage={canManage}
onOpen={() => navigate(`${BASE_PATH}/${node.id}`)}
onEdit={() => setFormMode({ kind: "edit", record: node })}
onDelete={() => setDeleteTarget(node)}
/>
))}
</Stack>
)}
</Card>
@@ -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 = () => {
>
<Stack gap="md">
<Text size="sm">
{deleteTarget && (childrenByParent.get(deleteTarget.id)?.length ?? 0) > 0 ? (
{deleteTarget && (childrenOf.get(deleteTarget.id)?.length ?? 0) > 0 ? (
<>
<Text span fw={600}>
{str(deleteTarget?.cargoTypeName)}
</Text>{" "}
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 (
<Box style={topBorder ? { borderTop: "1px solid var(--mantine-color-gray-2)" } : undefined}>
<Group
justify="space-between"
wrap="nowrap"
px="lg"
py="md"
style={{ background: open ? "var(--mantine-color-teal-0)" : undefined }}
>
<UnstyledButton
onClick={onToggle}
style={{ flex: 1, minWidth: 0 }}
aria-expanded={open}
>
<Group gap="sm" wrap="nowrap">
<ChevronRight
size={18}
style={{
color: "var(--mantine-color-gray-6)",
transition: "transform 150ms ease",
transform: open ? "rotate(90deg)" : "none",
flexShrink: 0,
}}
/>
<ThemeIcon size={36} radius="md" variant="light" color={inactive ? "gray" : "teal"}>
<Boxes size={18} />
</ThemeIcon>
<Box style={{ minWidth: 0 }}>
<Group gap={8} wrap="nowrap">
<Text fw={700} fz={15} c="dark.8" truncate>
{str(parent.cargoTypeName) || "Untitled"}
</Text>
{parent.code ? (
<Badge size="xs" variant="default" radius="sm">
{str(parent.code)}
</Badge>
) : null}
{inactive ? (
<Badge size="xs" variant="light" color="gray" radius="sm">
Inactive
</Badge>
) : null}
</Group>
<Text fz={12.5} c="dimmed" mt={2}>
{childCount} cargo type{childCount === 1 ? "" : "s"}
</Text>
</Box>
</Group>
</UnstyledButton>
<Group gap={4} wrap="nowrap">
{canManage && (
<Tooltip label="Add cargo type" withArrow>
<Button
size="compact-sm"
variant="light"
color="teal"
leftSection={<Plus size={14} />}
onClick={onAddChild}
>
Add
</Button>
</Tooltip>
)}
{canManage && (
<>
<Tooltip label="Edit category" withArrow>
<Button size="compact-sm" variant="subtle" color="gray" onClick={onEdit} px={8}>
<Pencil size={15} />
</Button>
</Tooltip>
<Tooltip label="Delete category" withArrow>
<Button size="compact-sm" variant="subtle" color="red" onClick={onDelete} px={8}>
<Trash2 size={15} />
</Button>
</Tooltip>
</>
)}
</Group>
</Group>
<Collapse in={open}>
<Box pb={open ? "xs" : 0}>{children}</Box>
</Collapse>
</Box>
);
}
// ── 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 (
<Group
justify="space-between"
wrap="nowrap"
pl={56}
pr="lg"
py="xs"
style={{ borderTop: "1px solid var(--mantine-color-gray-1)" }}
px="lg"
py="md"
style={{
borderTop: topBorder ? "1px solid var(--mantine-color-gray-2)" : undefined,
transition: "background 120ms ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "var(--mantine-color-teal-0)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "";
}}
>
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
<CornerDownRight size={15} style={{ color: "var(--mantine-color-gray-5)", flexShrink: 0 }} />
<Text fw={500} fz={14} c={inactive ? "dimmed" : "dark.7"} truncate>
{str(child.cargoTypeName) || "Untitled"}
</Text>
{child.code ? (
<Badge size="xs" variant="default" radius="sm">
{str(child.code)}
</Badge>
) : null}
{child.requiresDirectorApproval ? (
<Tooltip label="Requires director approval" withArrow>
<Badge
size="xs"
variant="light"
color="orange"
radius="sm"
leftSection={<ShieldCheck size={11} />}
>
Approval
</Badge>
</Tooltip>
) : null}
{child.showFreeTextBox ? (
<Tooltip label="Shows a free-text box on booking" withArrow>
<Badge
size="xs"
variant="light"
color="blue"
radius="sm"
leftSection={<FileText size={11} />}
>
Free text
</Badge>
</Tooltip>
) : null}
{inactive ? (
<Badge size="xs" variant="light" color="gray" radius="sm">
Inactive
</Badge>
) : null}
</Group>
{canManage && (
<Group gap={4} wrap="nowrap">
<Tooltip label="Edit" withArrow>
<Button size="compact-sm" variant="subtle" color="gray" onClick={onEdit} px={8}>
<Pencil size={14} />
</Button>
</Tooltip>
<Tooltip label="Delete" withArrow>
<Button size="compact-sm" variant="subtle" color="red" onClick={onDelete} px={8}>
<Trash2 size={14} />
</Button>
</Tooltip>
<UnstyledButton onClick={onOpen} style={{ flex: 1, minWidth: 0 }}>
<Group gap="sm" wrap="nowrap">
<ThemeIcon size={36} radius="md" variant="light" color={inactive ? "gray" : "teal"}>
{hasChildren ? <Layers size={18} /> : <Package size={18} />}
</ThemeIcon>
<Box style={{ minWidth: 0 }}>
<Group gap={8} wrap="nowrap">
<Text fw={650} fz={15} c="dark.8" truncate>
{str(node.cargoTypeName) || "Untitled"}
</Text>
{node.code ? (
<Badge size="xs" variant="default" radius="sm">
{str(node.code)}
</Badge>
) : null}
{node.requiresDirectorApproval ? (
<Tooltip label="Requires director approval" withArrow>
<Badge
size="xs"
variant="light"
color="orange"
radius="sm"
leftSection={<ShieldCheck size={11} />}
>
Approval
</Badge>
</Tooltip>
) : null}
{node.showFreeTextBox ? (
<Tooltip label="Shows a free-text box on booking" withArrow>
<Badge
size="xs"
variant="light"
color="blue"
radius="sm"
leftSection={<FileText size={11} />}
>
Free text
</Badge>
</Tooltip>
) : null}
{inactive ? (
<Badge size="xs" variant="light" color="gray" radius="sm">
Inactive
</Badge>
) : null}
</Group>
<Text fz={12.5} c="dimmed" mt={2}>
{hasChildren
? `${childCount} cargo type${childCount === 1 ? "" : "s"} inside`
: "No cargo types inside yet — open to add"}
</Text>
</Box>
</Group>
)}
</UnstyledButton>
<Group gap={4} wrap="nowrap">
{canManage && (
<>
<Tooltip label="Edit" withArrow>
<Button size="compact-sm" variant="subtle" color="gray" onClick={onEdit} px={8}>
<Pencil size={15} />
</Button>
</Tooltip>
<Tooltip label="Delete" withArrow>
<Button size="compact-sm" variant="subtle" color="red" onClick={onDelete} px={8}>
<Trash2 size={15} />
</Button>
</Tooltip>
</>
)}
<Tooltip label="Open" withArrow>
<Button size="compact-sm" variant="subtle" color="teal" onClick={onOpen} px={8}>
<ChevronRight size={18} />
</Button>
</Tooltip>
</Group>
</Group>
);
}