Fix: Throughput API v1 available - Details posted to Irving. POWERBI_KEY was missing from the .ENV file, so was not live.
Add: Editor now supports editing a mix's resolved formula directly, with % and kg dual entry on ingredient rows
Fix: Mix Editor should bring through correct ingredients. New resolved formula (same logic we use in Mix Calculator).
Fix: Security headers on all API responses (hardening)
Add: New mix button available on the Mix Editor.
Add: New ingredient button available on the Ingredient Editor
This commit is contained in:
2026-06-16 14:43:17 +12:00
parent 8f9a7b8193
commit 7db95e2027
46 changed files with 3805 additions and 1049 deletions
@@ -13,6 +13,7 @@ import {
ShieldCheck,
ShoppingCart,
SlidersHorizontal,
Settings,
Tags,
TrendingUp,
Users
@@ -90,6 +91,12 @@ export type Crumb = {
href?: string;
};
export type PageMeta = {
title: string;
category: string;
icon: ComponentType;
};
export const dashboardItem: NavItem = {
href: '/',
label: 'Dashboard',
@@ -285,9 +292,10 @@ export const baseSearchItems: SearchItem[] = [
* 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.
* Workflow-family layout: Dashboard, then an "Operations" group (the calculator,
* costing, editor, master tools, and throughput), then Ordering and Insights
* modules. Costing tools live inside Operations for the time being until they
* grow into a family of their own.
*/
export function buildClientNavEntries(visible: {
dashboard?: NavItem | null;
@@ -302,10 +310,14 @@ export function buildClientNavEntries(visible: {
entries.push({ kind: 'item', item: visible.dashboard });
}
if (visible.costing.length) {
const operationsChildren = [
...visible.costing,
...(visible.throughput ? [visible.throughput] : [])
];
if (operationsChildren.length) {
entries.push({
kind: 'group',
group: { id: 'costing', label: 'Costing', icon: Layers, children: visible.costing }
group: { id: 'operations', label: 'Operations', icon: Layers, children: operationsChildren }
});
}
@@ -313,10 +325,6 @@ export function buildClientNavEntries(visible: {
entries.push(visible.ordering);
}
if (visible.throughput) {
entries.push({ kind: 'item', item: visible.throughput });
}
if (visible.reporting) {
entries.push({ kind: 'item', item: visible.reporting });
}
@@ -358,14 +366,88 @@ export function findOrderingSection(pathname: string): NavItem | null {
}
export function pageTitle(pathname: string) {
return pageMeta(pathname).title;
}
export function pageCategory(pathname: string) {
return pageMeta(pathname).category;
}
export function pageMeta(pathname: string): PageMeta {
if (pathname === '/') {
return { title: 'Dashboard', category: 'Overview', icon: dashboardItem.icon };
}
if (pathname.startsWith('/ordering/manage')) {
const section = findOrderingSection(pathname);
return section && section.href !== '/ordering/manage'
? `Order Management · ${section.label}`
: 'Order Management';
return {
title: section?.label ?? 'Orders',
category: 'Order Management',
icon: section?.icon ?? orderingManageGroup.icon
};
}
if (pathname.startsWith('/ordering')) return 'Ordering';
return clientNavigationItems.find((item) => matchesRoute(item.href, pathname))?.label ?? 'Dashboard';
if (pathname.startsWith('/ordering')) {
return { title: 'Ordering', category: 'Ordering', icon: orderingItem.icon };
}
if (pathname.startsWith('/throughput/add')) {
return { title: 'Add Entry', category: 'Operations', icon: throughputItem.icon };
}
if (pathname.startsWith('/throughput')) {
return { title: throughputItem.label, category: 'Operations', icon: throughputItem.icon };
}
if (pathname.startsWith('/reporting')) {
return { title: reportingItem.label, category: 'Insights', icon: reportingItem.icon };
}
if (pathname.startsWith('/mix-calculator')) {
return { title: mixCalculatorItem.label, category: 'Operations', icon: mixCalculatorItem.icon };
}
if (pathname.startsWith('/product-costing')) {
return { title: productCostingItem.label, category: 'Operations', icon: productCostingItem.icon };
}
if (pathname.startsWith('/editor')) {
return { title: editorItem.label, category: 'Operations', icon: editorItem.icon };
}
if (pathname.startsWith('/ingredients')) {
return { title: ingredientsEditorItem.label, category: 'Operations', icon: ingredientsEditorItem.icon };
}
if (pathname.startsWith('/raw-materials')) {
return { title: 'Raw Materials', category: 'Operations', icon: FlaskConical };
}
if (pathname.startsWith('/products')) {
return { title: 'Products', category: 'Operations', icon: Package };
}
if (pathname.startsWith('/mixes/new')) {
return { title: 'New Mix', category: 'Operations', icon: ClipboardPenLine };
}
if (pathname.startsWith('/mixes')) {
return { title: 'Mix Master', category: 'Operations', icon: Layers };
}
if (pathname.startsWith('/scenarios')) {
return { title: 'Scenarios', category: 'Operations', icon: Layers };
}
if (pathname.startsWith('/client-access')) {
return { title: accessControlItem.label, category: 'Administration', icon: accessControlItem.icon };
}
if (pathname.startsWith('/settings')) {
return { title: 'Settings', category: 'Workspace', icon: Settings };
}
return { title: 'Workspace', category: 'Workspace', icon: LayoutDashboard };
}
export function clientBreadcrumbs(pathname: string, session?: AppSession | null): Crumb[] {