2026-05-08 23:07:01 +12:00
|
|
|
import {
|
2026-06-09 21:28:53 +12:00
|
|
|
BadgeDollarSign,
|
2026-05-08 23:07:01 +12:00
|
|
|
Calculator,
|
2026-06-03 00:17:12 +12:00
|
|
|
ClipboardPenLine,
|
2026-06-13 10:00:15 +12:00
|
|
|
FlaskConical,
|
2026-05-31 20:19:44 +12:00
|
|
|
Gauge,
|
2026-06-09 21:28:53 +12:00
|
|
|
Layers,
|
2026-05-08 23:07:01 +12:00
|
|
|
LayoutDashboard,
|
2026-06-13 10:00:15 +12:00
|
|
|
Link2,
|
|
|
|
|
ListOrdered,
|
|
|
|
|
Package,
|
|
|
|
|
Plug,
|
2026-05-08 23:07:01 +12:00
|
|
|
ShieldCheck,
|
2026-06-11 23:56:02 +12:00
|
|
|
ShoppingCart,
|
2026-06-13 10:00:15 +12:00
|
|
|
SlidersHorizontal,
|
|
|
|
|
Tags,
|
|
|
|
|
TrendingUp,
|
|
|
|
|
Users
|
2026-05-08 23:07:01 +12:00
|
|
|
} from 'lucide-svelte';
|
|
|
|
|
import type { ComponentType } from 'svelte';
|
|
|
|
|
|
|
|
|
|
import { featureFlags } from '$lib/features';
|
2026-05-31 20:19:44 +12:00
|
|
|
import {
|
|
|
|
|
canOpenDashboard
|
|
|
|
|
} from '$lib/workspace-access';
|
|
|
|
|
import type { AppSession } from '$lib/session';
|
2026-05-08 23:07:01 +12:00
|
|
|
|
|
|
|
|
export type SearchItem = {
|
|
|
|
|
href: string;
|
|
|
|
|
label: string;
|
|
|
|
|
description: string;
|
|
|
|
|
keywords: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type NavItem = {
|
|
|
|
|
href: string;
|
|
|
|
|
label: string;
|
|
|
|
|
shortLabel: string;
|
|
|
|
|
icon: ComponentType;
|
|
|
|
|
moduleKey?: string;
|
2026-05-31 20:19:44 +12:00
|
|
|
badge?: string;
|
2026-06-13 10:00:15 +12:00
|
|
|
/**
|
|
|
|
|
* Highlight this row only on an exact pathname match instead of a prefix
|
|
|
|
|
* match. Needed for parent routes like `/ordering/manage` that are a prefix
|
|
|
|
|
* of their siblings (`/ordering/manage/products`).
|
|
|
|
|
*/
|
|
|
|
|
exact?: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* Optional third-level submenu. A child with `children` renders as its own
|
|
|
|
|
* collapsible row inside a group (e.g. Integrations → Xero). The row stays a
|
|
|
|
|
* link to its own `href`; a chevron toggles the nested list.
|
|
|
|
|
*/
|
|
|
|
|
children?: NavItem[];
|
2026-05-08 23:07:01 +12:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type FooterLink = {
|
|
|
|
|
href: string;
|
|
|
|
|
label: string;
|
|
|
|
|
shortLabel: string;
|
|
|
|
|
icon: ComponentType;
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-09 21:28:53 +12:00
|
|
|
/**
|
|
|
|
|
* A collapsible family of related modules in the primary rail. Groups keep the
|
|
|
|
|
* top level short as more modules ship: a new costing tool becomes another child
|
|
|
|
|
* here rather than another peer in a flat stack.
|
|
|
|
|
*/
|
|
|
|
|
export type NavGroup = {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
icon: ComponentType;
|
|
|
|
|
children: NavItem[];
|
2026-06-13 10:00:15 +12:00
|
|
|
/**
|
|
|
|
|
* When set, the group header is itself a link (clicking it navigates here)
|
|
|
|
|
* while a separate chevron still toggles the child list. Used by Order
|
|
|
|
|
* Management: clicking the header lands on the order queue.
|
|
|
|
|
*/
|
|
|
|
|
href?: string;
|
|
|
|
|
/** Exact-match the header link's active state (see NavItem.exact). */
|
|
|
|
|
exact?: boolean;
|
2026-06-09 21:28:53 +12:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** The rail is a sequence of standalone items and collapsible groups. */
|
|
|
|
|
export type NavEntry =
|
|
|
|
|
| { kind: 'item'; item: NavItem }
|
|
|
|
|
| { kind: 'group'; group: NavGroup };
|
|
|
|
|
|
2026-05-08 23:07:01 +12:00
|
|
|
export type Crumb = {
|
|
|
|
|
label: string;
|
|
|
|
|
href?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const dashboardItem: NavItem = {
|
|
|
|
|
href: '/',
|
|
|
|
|
label: 'Dashboard',
|
|
|
|
|
shortLabel: 'DB',
|
|
|
|
|
icon: LayoutDashboard,
|
|
|
|
|
moduleKey: 'dashboard'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const mixCalculatorItem: NavItem = {
|
2026-05-31 20:19:44 +12:00
|
|
|
href: '/mix-calculator',
|
2026-05-08 23:07:01 +12:00
|
|
|
label: 'Mix Calculator',
|
|
|
|
|
shortLabel: 'MC',
|
|
|
|
|
icon: Calculator,
|
|
|
|
|
moduleKey: 'mix_calculator'
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-09 21:28:53 +12:00
|
|
|
export const productCostingItem: NavItem = {
|
|
|
|
|
href: '/product-costing',
|
|
|
|
|
label: 'Product Costing',
|
|
|
|
|
shortLabel: 'PC',
|
|
|
|
|
icon: BadgeDollarSign,
|
|
|
|
|
moduleKey: 'products',
|
|
|
|
|
badge: 'Alpha'
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-03 00:17:12 +12:00
|
|
|
export const editorItem: NavItem = {
|
|
|
|
|
href: '/editor',
|
2026-06-09 21:28:53 +12:00
|
|
|
label: 'Mix Editor',
|
2026-06-03 00:17:12 +12:00
|
|
|
shortLabel: 'ED',
|
|
|
|
|
icon: ClipboardPenLine,
|
|
|
|
|
moduleKey: 'products',
|
|
|
|
|
badge: 'test'
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-13 10:00:15 +12:00
|
|
|
export const ingredientsEditorItem: NavItem = {
|
|
|
|
|
href: '/ingredients',
|
|
|
|
|
label: 'Ingredients Editor',
|
|
|
|
|
shortLabel: 'IE',
|
|
|
|
|
icon: FlaskConical,
|
|
|
|
|
moduleKey: 'products',
|
|
|
|
|
badge: 'test'
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-08 23:07:01 +12:00
|
|
|
export const reportingItem: NavItem = {
|
|
|
|
|
href: '/reporting',
|
|
|
|
|
label: 'Reporting',
|
|
|
|
|
shortLabel: 'RP',
|
|
|
|
|
icon: TrendingUp,
|
|
|
|
|
moduleKey: 'products'
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-31 20:19:44 +12:00
|
|
|
export const throughputItem: NavItem = {
|
|
|
|
|
href: '/throughput',
|
|
|
|
|
label: 'Throughput',
|
|
|
|
|
shortLabel: 'OT',
|
|
|
|
|
icon: Gauge,
|
|
|
|
|
moduleKey: 'operations_throughput',
|
|
|
|
|
badge: 'test'
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-11 23:56:02 +12:00
|
|
|
export const orderingItem: NavItem = {
|
|
|
|
|
href: '/ordering',
|
|
|
|
|
label: 'Ordering',
|
|
|
|
|
shortLabel: 'OR',
|
|
|
|
|
icon: ShoppingCart,
|
|
|
|
|
moduleKey: 'ordering'
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-13 10:00:15 +12:00
|
|
|
/** Third-level submenu under Integrations. Each connected system is its own row. */
|
|
|
|
|
export const integrationsChildren: NavItem[] = [
|
|
|
|
|
{ href: '/ordering/manage/integrations/xero', label: 'Xero', shortLabel: 'XE', icon: Link2, moduleKey: 'ordering' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Children of the internal "Order Management" family. The first entry points at
|
|
|
|
|
* the management root (`/ordering/manage`) which renders the order queue, so it
|
|
|
|
|
* needs `exact` matching to avoid lighting up on its sibling routes. Integrations
|
|
|
|
|
* is itself a parent: it links to the integrations landing and expands to its
|
|
|
|
|
* connected systems (Xero) as a third sidebar layer.
|
|
|
|
|
*/
|
|
|
|
|
export const orderingManageChildren: NavItem[] = [
|
|
|
|
|
{ href: '/ordering/manage', label: 'Orders', shortLabel: 'OQ', icon: ListOrdered, moduleKey: 'ordering', exact: true },
|
|
|
|
|
{ href: '/ordering/manage/products', label: 'Products', shortLabel: 'PR', icon: Package, moduleKey: 'ordering' },
|
|
|
|
|
{ href: '/ordering/manage/customers', label: 'Customers', shortLabel: 'CU', icon: Users, moduleKey: 'ordering' },
|
|
|
|
|
{ href: '/ordering/manage/pricing', label: 'Pricing', shortLabel: 'PX', icon: Tags, moduleKey: 'ordering' },
|
|
|
|
|
{ href: '/ordering/manage/settings', label: 'Settings', shortLabel: 'ST', icon: SlidersHorizontal, moduleKey: 'ordering' },
|
|
|
|
|
{ href: '/ordering/manage/integrations', label: 'Integrations', shortLabel: 'IN', icon: Plug, moduleKey: 'ordering', exact: true, children: integrationsChildren }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/** The collapsible Order Management family for internal staff. */
|
|
|
|
|
export const orderingManageGroup: NavGroup = {
|
|
|
|
|
id: 'ordering',
|
|
|
|
|
label: 'Order Management',
|
|
|
|
|
icon: ShoppingCart,
|
|
|
|
|
href: '/ordering/manage',
|
|
|
|
|
exact: true,
|
|
|
|
|
children: orderingManageChildren
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-08 23:07:01 +12:00
|
|
|
export const workingDocumentItems: NavItem[] = [
|
2026-06-03 15:09:21 +12:00
|
|
|
// Mix Master remains available through the existing route and access logic,
|
|
|
|
|
// but is temporarily hidden from the sidebar.
|
2026-05-08 23:07:01 +12:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const accessControlItem: NavItem = {
|
|
|
|
|
href: '/client-access',
|
|
|
|
|
label: 'Client Access',
|
|
|
|
|
shortLabel: 'AC',
|
|
|
|
|
icon: ShieldCheck,
|
|
|
|
|
moduleKey: 'client_access'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const clientNavigationItems: NavItem[] = [
|
|
|
|
|
dashboardItem,
|
|
|
|
|
mixCalculatorItem,
|
2026-06-09 21:28:53 +12:00
|
|
|
productCostingItem,
|
2026-05-31 20:19:44 +12:00
|
|
|
throughputItem,
|
2026-06-03 00:17:12 +12:00
|
|
|
editorItem,
|
2026-06-13 10:00:15 +12:00
|
|
|
ingredientsEditorItem,
|
2026-05-08 23:07:01 +12:00
|
|
|
accessControlItem
|
|
|
|
|
];
|
|
|
|
|
|
2026-06-03 00:17:12 +12:00
|
|
|
export const footerLinks: FooterLink[] = [];
|
2026-05-08 23:07:01 +12:00
|
|
|
|
|
|
|
|
export const baseSearchItems: SearchItem[] = [
|
2026-06-09 21:28:53 +12:00
|
|
|
{
|
|
|
|
|
href: '/product-costing',
|
|
|
|
|
label: 'Open Product Costing',
|
|
|
|
|
description: 'Maintain product costing records, assumptions, and calculated pricing.',
|
|
|
|
|
keywords: 'alpha product costing pricing finished delivered distributor wholesale margin spreadsheet'
|
|
|
|
|
},
|
2026-06-03 00:17:12 +12:00
|
|
|
{
|
|
|
|
|
href: '/editor',
|
2026-06-09 21:28:53 +12:00
|
|
|
label: 'Open Mix Editor',
|
2026-06-13 10:00:15 +12:00
|
|
|
description: 'Edit mix names, status, and ingredients from one table.',
|
|
|
|
|
keywords: 'editor mixes clients names status ingredients recipe table phf horse manning'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: '/ingredients',
|
|
|
|
|
label: 'Open Ingredients Editor',
|
|
|
|
|
description: 'Curate the raw material ingredients available to mixes.',
|
|
|
|
|
keywords: 'ingredients editor raw materials supplier unit kg per unit catalogue mixes'
|
2026-06-03 00:17:12 +12:00
|
|
|
},
|
2026-05-08 23:07:01 +12:00
|
|
|
{
|
|
|
|
|
href: '/',
|
|
|
|
|
label: 'Open Dashboard',
|
|
|
|
|
description: 'Jump to the Hunter Premium Produce workspace summary.',
|
|
|
|
|
keywords: 'hunter premium produce overview dashboard workspace home'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: '/mixes',
|
|
|
|
|
label: 'Open Mix Master',
|
|
|
|
|
description: 'Browse saved mixes and their costing outputs.',
|
|
|
|
|
keywords: 'mix master mixes recipes spreadsheet'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: '/mixes/new',
|
|
|
|
|
label: 'Create New Mix',
|
|
|
|
|
description: 'Start a new costing worksheet for Hunter Premium Produce.',
|
|
|
|
|
keywords: 'new mix create worksheet hunter premium produce formula'
|
|
|
|
|
},
|
|
|
|
|
...(featureFlags.mixCalculatorSessionHistory
|
|
|
|
|
? [
|
|
|
|
|
{
|
|
|
|
|
href: '/mix-calculator',
|
|
|
|
|
label: 'Open Mix Calculator',
|
|
|
|
|
description: 'Review saved production sessions and batch calculations.',
|
|
|
|
|
keywords: 'mix calculator production sessions batch bags client product'
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
: []),
|
|
|
|
|
{
|
2026-05-31 20:19:44 +12:00
|
|
|
href: '/mix-calculator',
|
|
|
|
|
label: 'Open Mix Calculator',
|
2026-05-08 23:07:01 +12:00
|
|
|
description: 'Run a new client-specific mix calculation session.',
|
|
|
|
|
keywords: 'new mix calculator session client batch size product bags print'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: '/reporting',
|
|
|
|
|
label: 'Open Reporting',
|
|
|
|
|
description: 'View raw material costs, mix summaries, product pricing, and data quality reports.',
|
|
|
|
|
keywords: 'reporting reports raw materials mix cost product pricing data quality price review'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: '/settings',
|
|
|
|
|
label: 'Open Workspace Settings',
|
|
|
|
|
description: 'Review account details and workspace preferences.',
|
|
|
|
|
keywords: 'settings account preferences profile workspace'
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2026-06-09 21:28:53 +12:00
|
|
|
/**
|
|
|
|
|
* Assemble the grouped primary rail from already access-filtered items.
|
|
|
|
|
* Callers pass only the modules the current session may see; empty families
|
|
|
|
|
* collapse away so a role with one costing tool never gets an empty group.
|
|
|
|
|
*
|
|
|
|
|
* Workflow-family layout: Dashboard, then a "Costing" group (the calculator,
|
|
|
|
|
* costing, editor, and master tools), then Operations and Insights modules at
|
|
|
|
|
* the top level until each grows into a family of its own.
|
|
|
|
|
*/
|
|
|
|
|
export function buildClientNavEntries(visible: {
|
|
|
|
|
dashboard?: NavItem | null;
|
|
|
|
|
costing: NavItem[];
|
|
|
|
|
throughput?: NavItem | null;
|
2026-06-13 10:00:15 +12:00
|
|
|
ordering?: NavEntry | null;
|
2026-06-09 21:28:53 +12:00
|
|
|
reporting?: NavItem | null;
|
|
|
|
|
}): NavEntry[] {
|
|
|
|
|
const entries: NavEntry[] = [];
|
|
|
|
|
|
|
|
|
|
if (visible.dashboard) {
|
|
|
|
|
entries.push({ kind: 'item', item: visible.dashboard });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (visible.costing.length) {
|
|
|
|
|
entries.push({
|
|
|
|
|
kind: 'group',
|
|
|
|
|
group: { id: 'costing', label: 'Costing', icon: Layers, children: visible.costing }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 23:56:02 +12:00
|
|
|
if (visible.ordering) {
|
2026-06-13 10:00:15 +12:00
|
|
|
entries.push(visible.ordering);
|
2026-06-11 23:56:02 +12:00
|
|
|
}
|
|
|
|
|
|
2026-06-09 21:28:53 +12:00
|
|
|
if (visible.throughput) {
|
|
|
|
|
entries.push({ kind: 'item', item: visible.throughput });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (visible.reporting) {
|
|
|
|
|
entries.push({ kind: 'item', item: visible.reporting });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return entries;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 10:00:15 +12:00
|
|
|
/** True when a row or any of its nested children matches the current route. */
|
|
|
|
|
function itemOrChildActive(item: NavItem, pathname: string): boolean {
|
|
|
|
|
if (matchesRoute(item.href, pathname, item.exact)) return true;
|
|
|
|
|
return item.children?.some((child) => matchesRoute(child.href, pathname, child.exact)) ?? false;
|
2026-06-09 21:28:53 +12:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 10:00:15 +12:00
|
|
|
/** True when any of a group's children (or grandchildren) matches the route. */
|
|
|
|
|
export function groupHasActiveChild(group: NavGroup, pathname: string) {
|
|
|
|
|
return group.children.some((child) => itemOrChildActive(child, pathname));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function matchesRoute(href: string, pathname: string, exact = false) {
|
|
|
|
|
if (href === '/') return pathname === '/';
|
|
|
|
|
if (exact) return pathname === href;
|
|
|
|
|
return pathname.startsWith(href);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolve the deepest Order Management section row for a path, descending into
|
|
|
|
|
* third-level submenus (Integrations → Xero) so headers and breadcrumbs name the
|
|
|
|
|
* actual page rather than the parent. Returns null for the management root.
|
|
|
|
|
*/
|
|
|
|
|
export function findOrderingSection(pathname: string): NavItem | null {
|
|
|
|
|
for (const child of orderingManageChildren) {
|
|
|
|
|
// Check grandchildren first so a nested page (Xero) wins over its parent.
|
|
|
|
|
for (const grandchild of child.children ?? []) {
|
|
|
|
|
if (matchesRoute(grandchild.href, pathname, grandchild.exact)) return grandchild;
|
|
|
|
|
}
|
|
|
|
|
if (matchesRoute(child.href, pathname, child.exact)) return child;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
2026-05-08 23:07:01 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function pageTitle(pathname: string) {
|
2026-06-13 10:00:15 +12:00
|
|
|
if (pathname.startsWith('/ordering/manage')) {
|
|
|
|
|
const section = findOrderingSection(pathname);
|
|
|
|
|
return section && section.href !== '/ordering/manage'
|
|
|
|
|
? `Order Management · ${section.label}`
|
|
|
|
|
: 'Order Management';
|
|
|
|
|
}
|
|
|
|
|
if (pathname.startsWith('/ordering')) return 'Ordering';
|
2026-05-08 23:07:01 +12:00
|
|
|
return clientNavigationItems.find((item) => matchesRoute(item.href, pathname))?.label ?? 'Dashboard';
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 20:19:44 +12:00
|
|
|
export function clientBreadcrumbs(pathname: string, session?: AppSession | null): Crumb[] {
|
|
|
|
|
const crumbs: Crumb[] = [];
|
|
|
|
|
|
|
|
|
|
if (canOpenDashboard(session)) {
|
|
|
|
|
crumbs.push({ label: 'Workspace', href: '/' });
|
|
|
|
|
}
|
2026-05-08 23:07:01 +12:00
|
|
|
|
|
|
|
|
if (pathname === '/') {
|
2026-05-31 20:19:44 +12:00
|
|
|
return crumbs.length ? [...crumbs, { label: 'Dashboard' }] : [{ label: 'Dashboard' }];
|
2026-05-08 23:07:01 +12:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pathname.startsWith('/mix-calculator')) {
|
2026-05-31 20:19:44 +12:00
|
|
|
return [...crumbs, { label: 'Mix Calculator' }];
|
2026-05-08 23:07:01 +12:00
|
|
|
}
|
|
|
|
|
|
2026-06-09 21:28:53 +12:00
|
|
|
if (pathname.startsWith('/product-costing')) {
|
|
|
|
|
return [...crumbs, { label: 'Product Costing' }];
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 00:17:12 +12:00
|
|
|
if (pathname.startsWith('/editor')) {
|
2026-06-09 21:28:53 +12:00
|
|
|
return [...crumbs, { label: 'Mix Editor' }];
|
2026-06-03 00:17:12 +12:00
|
|
|
}
|
|
|
|
|
|
2026-05-08 23:07:01 +12:00
|
|
|
if (pathname.startsWith('/mixes')) {
|
2026-05-31 20:19:44 +12:00
|
|
|
return [...crumbs, { label: 'Mix Master' }];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pathname.startsWith('/throughput')) {
|
|
|
|
|
const base: Crumb[] = [...crumbs, { label: 'Throughput', href: '/throughput' }];
|
|
|
|
|
if (pathname === '/throughput/add') return [...base, { label: 'Add Entry' }];
|
|
|
|
|
if (pathname === '/throughput') return base.slice(0, -1).concat([{ label: 'Throughput' }]);
|
|
|
|
|
return base;
|
2026-05-08 23:07:01 +12:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 10:00:15 +12:00
|
|
|
if (pathname.startsWith('/ordering/manage')) {
|
|
|
|
|
const section = findOrderingSection(pathname);
|
|
|
|
|
if (!section || section.href === '/ordering/manage') {
|
|
|
|
|
return [...crumbs, { label: 'Order Management' }];
|
|
|
|
|
}
|
|
|
|
|
const chain: Crumb[] = [...crumbs, { label: 'Order Management', href: '/ordering/manage' }];
|
|
|
|
|
// When the section is a nested grandchild (e.g. Xero), include its parent
|
|
|
|
|
// (Integrations) as an intermediate crumb.
|
|
|
|
|
const parent = orderingManageChildren.find((c) => c.children?.includes(section));
|
|
|
|
|
if (parent) chain.push({ label: parent.label, href: parent.href });
|
|
|
|
|
chain.push({ label: section.label });
|
|
|
|
|
return chain;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 23:07:01 +12:00
|
|
|
const sectionMap: Record<string, string> = {
|
|
|
|
|
'/raw-materials': 'Raw Materials',
|
2026-06-09 21:28:53 +12:00
|
|
|
'/product-costing': 'Product Costing',
|
2026-05-08 23:07:01 +12:00
|
|
|
'/products': 'Products',
|
|
|
|
|
'/scenarios': 'Scenarios',
|
|
|
|
|
'/client-access': 'Client Access',
|
|
|
|
|
'/reporting': 'Reporting',
|
2026-05-31 20:19:44 +12:00
|
|
|
'/settings': 'Settings',
|
|
|
|
|
'/throughput': 'Throughput'
|
2026-05-08 23:07:01 +12:00
|
|
|
};
|
|
|
|
|
const section = sectionMap[pathname];
|
2026-05-31 20:19:44 +12:00
|
|
|
if (section) return [...crumbs, { label: section }];
|
2026-05-08 23:07:01 +12:00
|
|
|
|
2026-05-31 20:19:44 +12:00
|
|
|
return [...crumbs, { label: pageTitle(pathname) }];
|
2026-05-08 23:07:01 +12:00
|
|
|
}
|