Files
data-entry-app/frontend/src/lib/navigation/client-navigation.ts
T

301 lines
8.0 KiB
TypeScript
Raw Normal View History

import {
2026-06-09 21:28:53 +12:00
BadgeDollarSign,
Calculator,
2026-06-03 00:17:12 +12:00
ClipboardPenLine,
2026-05-31 20:19:44 +12:00
Gauge,
2026-06-09 21:28:53 +12:00
Layers,
LayoutDashboard,
ShieldCheck,
2026-06-03 00:17:12 +12:00
TrendingUp
} 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';
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;
};
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[];
};
/** The rail is a sequence of standalone items and collapsible groups. */
export type NavEntry =
| { kind: 'item'; item: NavItem }
| { kind: 'group'; group: NavGroup };
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',
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'
};
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'
};
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.
];
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,
accessControlItem
];
2026-06-03 00:17:12 +12:00
export const footerLinks: FooterLink[] = [];
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-03 00:17:12 +12:00
description: 'Edit client, product, and mix naming from one table.',
keywords: 'editor products mixes clients names bulk table phf horse manning'
},
{
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',
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;
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 }
});
}
if (visible.throughput) {
entries.push({ kind: 'item', item: visible.throughput });
}
if (visible.reporting) {
entries.push({ kind: 'item', item: visible.reporting });
}
return entries;
}
/** True when any of a group's children matches the current route. */
export function groupHasActiveChild(group: NavGroup, pathname: string) {
return group.children.some((child) => matchesRoute(child.href, pathname));
}
export function matchesRoute(href: string, pathname: string) {
return href === '/' ? pathname === '/' : pathname.startsWith(href);
}
export function pageTitle(pathname: string) {
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: '/' });
}
if (pathname === '/') {
2026-05-31 20:19:44 +12:00
return crumbs.length ? [...crumbs, { label: 'Dashboard' }] : [{ label: 'Dashboard' }];
}
if (pathname.startsWith('/mix-calculator')) {
2026-05-31 20:19:44 +12:00
return [...crumbs, { label: 'Mix Calculator' }];
}
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
}
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;
}
const sectionMap: Record<string, string> = {
'/raw-materials': 'Raw Materials',
2026-06-09 21:28:53 +12:00
'/product-costing': 'Product Costing',
'/products': 'Products',
'/scenarios': 'Scenarios',
'/client-access': 'Client Access',
'/reporting': 'Reporting',
2026-05-31 20:19:44 +12:00
'/settings': 'Settings',
'/throughput': 'Throughput'
};
const section = sectionMap[pathname];
2026-05-31 20:19:44 +12:00
if (section) return [...crumbs, { label: section }];
2026-05-31 20:19:44 +12:00
return [...crumbs, { label: pageTitle(pathname) }];
}