v0.1.19 - Throughput overview & responsive header
- Throughput: new "Throughput Overview" header with Gauge icon and brand-green badge icons on each card (Today, This week, 4-week average, Horse Mix, Grain Mix) - Throughput: inline rolling-range selector (7d / 4w / 6w / 12w, default 4 weeks) driving the customer-mix cards; stats window widened to 12 weeks so switching range is a pure client-side re-filter - Throughput: cards collapse to a single even 5-across row on laptop and up, with container-query value text that scales to each card's width - Throughput: date logic pinned to Australian Eastern time (fixes the day-early date); This week subtitle shows the Mon-Sun date range - Throughput: subtler tinted add-form; removed the inline-entry kicker and the "Open full form" link - Topbar: fix cramped laptop header - action toggles no longer wrap above the user button; search drops to its own row earlier Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,20 @@ import {
|
||||
BadgeDollarSign,
|
||||
Calculator,
|
||||
ClipboardPenLine,
|
||||
FlaskConical,
|
||||
Gauge,
|
||||
Layers,
|
||||
LayoutDashboard,
|
||||
Link2,
|
||||
ListOrdered,
|
||||
Package,
|
||||
Plug,
|
||||
ShieldCheck,
|
||||
ShoppingCart,
|
||||
TrendingUp
|
||||
SlidersHorizontal,
|
||||
Tags,
|
||||
TrendingUp,
|
||||
Users
|
||||
} from 'lucide-svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
@@ -31,6 +39,18 @@ export type NavItem = {
|
||||
icon: ComponentType;
|
||||
moduleKey?: string;
|
||||
badge?: string;
|
||||
/**
|
||||
* 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[];
|
||||
};
|
||||
|
||||
export type FooterLink = {
|
||||
@@ -50,6 +70,14 @@ export type NavGroup = {
|
||||
label: string;
|
||||
icon: ComponentType;
|
||||
children: NavItem[];
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/** The rail is a sequence of standalone items and collapsible groups. */
|
||||
@@ -96,6 +124,15 @@ export const editorItem: NavItem = {
|
||||
badge: 'test'
|
||||
};
|
||||
|
||||
export const ingredientsEditorItem: NavItem = {
|
||||
href: '/ingredients',
|
||||
label: 'Ingredients Editor',
|
||||
shortLabel: 'IE',
|
||||
icon: FlaskConical,
|
||||
moduleKey: 'products',
|
||||
badge: 'test'
|
||||
};
|
||||
|
||||
export const reportingItem: NavItem = {
|
||||
href: '/reporting',
|
||||
label: 'Reporting',
|
||||
@@ -121,6 +158,37 @@ export const orderingItem: NavItem = {
|
||||
moduleKey: 'ordering'
|
||||
};
|
||||
|
||||
/** 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
|
||||
};
|
||||
|
||||
export const workingDocumentItems: NavItem[] = [
|
||||
// Mix Master remains available through the existing route and access logic,
|
||||
// but is temporarily hidden from the sidebar.
|
||||
@@ -140,6 +208,7 @@ export const clientNavigationItems: NavItem[] = [
|
||||
productCostingItem,
|
||||
throughputItem,
|
||||
editorItem,
|
||||
ingredientsEditorItem,
|
||||
accessControlItem
|
||||
];
|
||||
|
||||
@@ -155,8 +224,14 @@ export const baseSearchItems: SearchItem[] = [
|
||||
{
|
||||
href: '/editor',
|
||||
label: 'Open Mix Editor',
|
||||
description: 'Edit client, product, and mix naming from one table.',
|
||||
keywords: 'editor products mixes clients names bulk table phf horse manning'
|
||||
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'
|
||||
},
|
||||
{
|
||||
href: '/',
|
||||
@@ -219,7 +294,7 @@ export function buildClientNavEntries(visible: {
|
||||
dashboard?: NavItem | null;
|
||||
costing: NavItem[];
|
||||
throughput?: NavItem | null;
|
||||
ordering?: NavItem | null;
|
||||
ordering?: NavEntry | null;
|
||||
reporting?: NavItem | null;
|
||||
}): NavEntry[] {
|
||||
const entries: NavEntry[] = [];
|
||||
@@ -236,7 +311,7 @@ export function buildClientNavEntries(visible: {
|
||||
}
|
||||
|
||||
if (visible.ordering) {
|
||||
entries.push({ kind: 'item', item: visible.ordering });
|
||||
entries.push(visible.ordering);
|
||||
}
|
||||
|
||||
if (visible.throughput) {
|
||||
@@ -250,16 +325,47 @@ export function buildClientNavEntries(visible: {
|
||||
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));
|
||||
/** 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;
|
||||
}
|
||||
|
||||
export function matchesRoute(href: string, pathname: string) {
|
||||
return href === '/' ? pathname === '/' : pathname.startsWith(href);
|
||||
/** 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;
|
||||
}
|
||||
|
||||
export function pageTitle(pathname: string) {
|
||||
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';
|
||||
return clientNavigationItems.find((item) => matchesRoute(item.href, pathname))?.label ?? 'Dashboard';
|
||||
}
|
||||
|
||||
@@ -297,6 +403,20 @@ export function clientBreadcrumbs(pathname: string, session?: AppSession | null)
|
||||
return base;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const sectionMap: Record<string, string> = {
|
||||
'/raw-materials': 'Raw Materials',
|
||||
'/product-costing': 'Product Costing',
|
||||
|
||||
Reference in New Issue
Block a user