Move working documents to its own area, rename dashboard

This commit is contained in:
2026-04-29 01:21:16 +12:00
parent 7e9663fa06
commit 761ebb050d
32 changed files with 1779 additions and 526 deletions
+163 -87
View File
@@ -2,7 +2,7 @@
import { invalidateAll } from '$app/navigation';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { clientSession, sessionHydrated } from '$lib/session';
import { clientSession, hasModuleAccess, sessionHydrated } from '$lib/session';
import { onMount, tick } from 'svelte';
import packageInfo from '../../../package.json';
@@ -18,22 +18,23 @@
label: string;
shortLabel: string;
icon?: 'home';
moduleKey?: string;
};
const dashboardItem: NavItem = { href: '/', label: 'Dashboard', shortLabel: 'DB', icon: 'home' };
const dashboardItem: NavItem = { href: '/', label: 'Dashboard', shortLabel: 'DB', icon: 'home', moduleKey: 'dashboard' };
const workingDocumentItems: NavItem[] = [
{ href: '/raw-materials', label: 'Raw Materials', shortLabel: 'RM' },
{ href: '/mixes', label: 'Mix Master', shortLabel: 'MM' },
{ href: '/products', label: 'Products', shortLabel: 'PR' },
{ href: '/scenarios', label: 'Scenarios', shortLabel: 'SC' }
{ href: '/raw-materials', label: 'Raw Materials', shortLabel: 'RM', moduleKey: 'raw_materials' },
{ href: '/mixes', label: 'Mix Master', shortLabel: 'MM', moduleKey: 'mix_master' },
{ href: '/products', label: 'Products', shortLabel: 'PR', moduleKey: 'products' },
{ href: '/scenarios', label: 'Scenarios', shortLabel: 'SC', moduleKey: 'scenarios' }
];
const navigation = [dashboardItem, ...workingDocumentItems];
const accessControlItem: NavItem = { href: '/client-access', label: 'Client Access', shortLabel: 'AC', moduleKey: 'client_access' };
const navigation = [dashboardItem, ...workingDocumentItems, accessControlItem];
const footerLinks = [
{ href: '/products', label: 'Delivered Pricing', shortLabel: 'DP' },
{ href: '/scenarios', label: 'Planning View', shortLabel: 'PV' }
];
const primaryBottomNavigation = [dashboardItem, ...workingDocumentItems.slice(0, 3)];
const searchItems: SearchItem[] = [
{
@@ -88,12 +89,30 @@
let quickMenuOpen = $state(false);
let userMenuOpen = $state(false);
let navOpen = $state(false);
let workingDocumentsExpanded = $state(true);
let showBottomNav = $state(false);
let isRestoringSession = $state(false);
let restoredToken = $state<string | null>(null);
let paletteInput: HTMLInputElement | null = $state(null);
const appVersion = `v${packageInfo.version}`;
const currentYear = new Date().getFullYear();
const visibleDashboardItem = $derived(
!$clientSession || !dashboardItem.moduleKey || hasModuleAccess($clientSession, dashboardItem.moduleKey) ? dashboardItem : null
);
const visibleWorkingDocumentItems = $derived(
!$clientSession
? workingDocumentItems
: workingDocumentItems.filter((item) => !item.moduleKey || hasModuleAccess($clientSession, item.moduleKey))
);
const visibleFooterLinks = $derived([
...footerLinks,
...(!$clientSession || !hasModuleAccess($clientSession, 'client_access', 'manage')
? []
: [{ href: accessControlItem.href, label: accessControlItem.label, shortLabel: accessControlItem.shortLabel }])
]);
const primaryBottomNavigation = $derived(
[...(visibleDashboardItem ? [visibleDashboardItem] : []), ...visibleWorkingDocumentItems.slice(0, 3)]
);
function matchesRoute(href: string, pathname: string) {
return href === '/' ? pathname === '/' : pathname.startsWith(href);
@@ -111,7 +130,8 @@
'/mixes/new': 'Create a new mix worksheet for Hunter Premium Produce',
'/products': 'Track delivered product pricing and margin views',
'/settings': 'Review your workspace profile and application settings',
'/scenarios': 'Compare alternate pricing and production assumptions'
'/scenarios': 'Compare alternate pricing and production assumptions',
'/client-access': 'Manage user access, module permissions, and audit history'
};
return descriptions[pathname] ?? 'Hunter Premium Produce client workspace';
@@ -256,50 +276,63 @@
</button>
<nav class="nav-list" aria-label="Client navigation">
<a class:active={matchesRoute(dashboardItem.href, page.url.pathname)} href={dashboardItem.href}>
<span class="nav-icon">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M3.75 10.5 12 3.75l8.25 6.75"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.25 9.75v9h13.5v-9"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M10.125 18.75v-5.25h3.75v5.25"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span>{dashboardItem.label}</span>
</a>
{#if visibleDashboardItem}
<a class:active={matchesRoute(visibleDashboardItem.href, page.url.pathname)} href={visibleDashboardItem.href}>
<span class="nav-icon">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M3.75 10.5 12 3.75l8.25 6.75"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.25 9.75v9h13.5v-9"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M10.125 18.75v-5.25h3.75v5.25"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span>{visibleDashboardItem.label}</span>
</a>
{/if}
</nav>
<div class="nav-group" aria-label="Working documents">
<p class="nav-group-label">Working Documents</p>
<nav class="nav-sublist" aria-label="Working document pages">
{#each workingDocumentItems as item}
<a class:active={matchesRoute(item.href, page.url.pathname)} href={item.href}>
<span class="nav-icon">{item.shortLabel}</span>
<span>{item.label}</span>
</a>
{/each}
</nav>
<div class="nav-group" aria-label="Working documents" hidden={!visibleWorkingDocumentItems.length}>
<button
aria-controls="working-documents-nav"
aria-expanded={workingDocumentsExpanded}
class="nav-group-toggle"
type="button"
onclick={() => (workingDocumentsExpanded = !workingDocumentsExpanded)}
>
<span class="nav-group-label">Working Documents</span>
<span class:open={workingDocumentsExpanded} class="chevron"></span>
</button>
{#if workingDocumentsExpanded}
<nav class="nav-sublist" id="working-documents-nav" aria-label="Working document pages">
{#each visibleWorkingDocumentItems as item}
<a class:active={matchesRoute(item.href, page.url.pathname)} href={item.href}>
<span class="nav-icon">{item.shortLabel}</span>
<span>{item.label}</span>
</a>
{/each}
</nav>
{/if}
</div>
<div class="sidebar-footer">
{#each footerLinks as item}
{#each visibleFooterLinks as item}
<a href={item.href}>
<span class="nav-icon muted">{item.shortLabel}</span>
<span>{item.label}</span>
@@ -498,43 +531,58 @@
<div class="drawer-grid">
<nav class="drawer-section" aria-label="All workspace pages">
<a class:active={matchesRoute(dashboardItem.href, page.url.pathname)} href={dashboardItem.href} onclick={() => (navOpen = false)}>
<span class="nav-icon">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M3.75 10.5 12 3.75l8.25 6.75"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.25 9.75v9h13.5v-9"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M10.125 18.75v-5.25h3.75v5.25"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span>{dashboardItem.label}</span>
</a>
{#if visibleDashboardItem}
<a class:active={matchesRoute(visibleDashboardItem.href, page.url.pathname)} href={visibleDashboardItem.href} onclick={() => (navOpen = false)}>
<span class="nav-icon">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M3.75 10.5 12 3.75l8.25 6.75"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5.25 9.75v9h13.5v-9"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M10.125 18.75v-5.25h3.75v5.25"
stroke="currentColor"
stroke-width="1.85"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span>{visibleDashboardItem.label}</span>
</a>
{/if}
<div class="drawer-group">
<p class="drawer-group-label">Working Documents</p>
{#each workingDocumentItems as item}
<a class:active={matchesRoute(item.href, page.url.pathname)} href={item.href} onclick={() => (navOpen = false)}>
<span class="nav-icon">{item.shortLabel}</span>
<span>{item.label}</span>
</a>
{/each}
<div class="drawer-group" hidden={!visibleWorkingDocumentItems.length}>
<button
aria-controls="drawer-working-documents-nav"
aria-expanded={workingDocumentsExpanded}
class="nav-group-toggle drawer-group-toggle"
type="button"
onclick={() => (workingDocumentsExpanded = !workingDocumentsExpanded)}
>
<span class="drawer-group-label">Working Documents</span>
<span class:open={workingDocumentsExpanded} class="chevron"></span>
</button>
{#if workingDocumentsExpanded}
<div id="drawer-working-documents-nav" class="drawer-sublist">
{#each visibleWorkingDocumentItems as item}
<a class:active={matchesRoute(item.href, page.url.pathname)} href={item.href} onclick={() => (navOpen = false)}>
<span class="nav-icon">{item.shortLabel}</span>
<span>{item.label}</span>
</a>
{/each}
</div>
{/if}
</div>
</nav>
@@ -565,7 +613,7 @@
</div>
<div class="drawer-footer">
{#each footerLinks as item}
{#each visibleFooterLinks as item}
<a href={item.href} onclick={() => (navOpen = false)}>
<span>{item.label}</span>
<small>{item.shortLabel}</small>
@@ -683,6 +731,10 @@
padding: 0.9rem;
background: var(--panel);
border-right: 1px solid var(--line);
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
}
.sidebar-body {
@@ -865,6 +917,19 @@
padding-top: 0.15rem;
}
.nav-group-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
width: 100%;
padding: 0 0.68rem;
border: none;
background: transparent;
color: inherit;
cursor: pointer;
}
.nav-group-label,
.drawer-group-label {
margin: 0;
@@ -876,7 +941,7 @@
}
.nav-group-label {
padding: 0 0.68rem;
padding: 0;
}
.nav-sublist {
@@ -917,6 +982,7 @@
.sidebar-footer {
margin-top: auto;
padding-top: 0.6rem;
flex-shrink: 0;
}
.sidebar-meta {
@@ -925,6 +991,7 @@
padding: 0.85rem 0.3rem 0;
color: var(--muted);
font-size: 0.78rem;
flex-shrink: 0;
}
.sidebar-meta small {
@@ -1429,10 +1496,19 @@
padding-top: 0.35rem;
}
.drawer-group-label {
.drawer-group-toggle {
padding: 0 0.2rem;
}
.drawer-group-label {
padding: 0;
}
.drawer-sublist {
display: grid;
gap: 0.4rem;
}
.drawer-footer {
display: grid;
gap: 0.5rem;