Deployment fixes, add HPP logo
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import Lean101Logo from '$lib/components/Lean101Logo.svelte';
|
||||
import { clientSession, hasModuleAccess, sessionHydrated } from '$lib/session';
|
||||
import { featureFlags } from '$lib/features';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import packageInfo from '../../../package.json';
|
||||
|
||||
@@ -24,7 +24,12 @@
|
||||
};
|
||||
|
||||
const dashboardItem: NavItem = { href: '/', label: 'Dashboard', shortLabel: 'DB', icon: 'home', moduleKey: 'dashboard' };
|
||||
const mixCalculatorItem: NavItem = { href: '/mix-calculator', label: 'Mix Calculator', shortLabel: 'MC', moduleKey: 'mix_calculator' };
|
||||
const mixCalculatorItem: NavItem = {
|
||||
href: featureFlags.mixCalculatorSessionHistory ? '/mix-calculator' : '/mix-calculator/new',
|
||||
label: 'Mix Calculator',
|
||||
shortLabel: 'MC',
|
||||
moduleKey: 'mix_calculator'
|
||||
};
|
||||
const workingDocumentItems: NavItem[] = [
|
||||
{ href: '/raw-materials', label: 'Raw Materials', shortLabel: 'RM', moduleKey: 'raw_materials' },
|
||||
{ href: '/mixes', label: 'Mix Master', shortLabel: 'MM', moduleKey: 'mix_master' },
|
||||
@@ -64,12 +69,16 @@
|
||||
description: 'Start a new costing worksheet for Hunter Premium Produce.',
|
||||
keywords: 'new mix create worksheet hunter premium produce formula'
|
||||
},
|
||||
{
|
||||
href: '/mix-calculator',
|
||||
label: 'Open Mix Calculator',
|
||||
description: 'Review saved production sessions and batch calculations.',
|
||||
keywords: 'mix calculator production sessions batch bags client product'
|
||||
},
|
||||
...(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'
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
href: '/mix-calculator/new',
|
||||
label: 'Create Mix Calculation',
|
||||
@@ -281,7 +290,9 @@
|
||||
Promise.all([
|
||||
hasModuleAccess(session, 'products') ? api.products() : Promise.resolve([]),
|
||||
hasModuleAccess(session, 'mix_master') ? api.mixes() : Promise.resolve([]),
|
||||
hasModuleAccess(session, 'mix_calculator') ? api.mixCalculatorSessions() : Promise.resolve([])
|
||||
featureFlags.mixCalculatorSessionHistory && hasModuleAccess(session, 'mix_calculator')
|
||||
? api.mixCalculatorSessions()
|
||||
: Promise.resolve([])
|
||||
])
|
||||
.then(([products, mixes, sessions]) => {
|
||||
if (seededSearchToken !== token) {
|
||||
@@ -382,7 +393,7 @@
|
||||
<aside class="sidebar">
|
||||
<div class="brand-row">
|
||||
<a class="brand" href="/">
|
||||
<Lean101Logo className="sidebar-logo" showTagline={false} />
|
||||
<img class="sidebar-logo" src="/logo-hsf.png" alt="Hunter Premium Produce" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -421,7 +432,9 @@
|
||||
|
||||
{#if visibleMixCalculatorItem}
|
||||
<a class:active={matchesRoute(visibleMixCalculatorItem.href, page.url.pathname)} href={visibleMixCalculatorItem.href}>
|
||||
<span class="nav-icon">{visibleMixCalculatorItem.shortLabel}</span>
|
||||
<span class="nav-icon">
|
||||
<span class="nav-icon-mask" style="--nav-icon-url: url('/icons/calculator.svg');" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span>{visibleMixCalculatorItem.label}</span>
|
||||
</a>
|
||||
{/if}
|
||||
@@ -560,7 +573,7 @@
|
||||
<div class="menu-panel quick-fab-panel">
|
||||
<a href="/mixes">Open mix costing</a>
|
||||
<a href="/mixes/new">Create mix worksheet</a>
|
||||
<a href="/mix-calculator">Open mix calculator</a>
|
||||
<a href={featureFlags.mixCalculatorSessionHistory ? '/mix-calculator' : '/mix-calculator/new'}>Open mix calculator</a>
|
||||
<a href="/mix-calculator/new">Create mix session</a>
|
||||
<a href="/products">Review delivered pricing</a>
|
||||
<button type="button" onclick={() => openPalette('')}>Search the workspace</button>
|
||||
@@ -687,7 +700,9 @@
|
||||
|
||||
{#if visibleMixCalculatorItem}
|
||||
<a class:active={matchesRoute(visibleMixCalculatorItem.href, page.url.pathname)} href={visibleMixCalculatorItem.href} onclick={() => (navOpen = false)}>
|
||||
<span class="nav-icon">{visibleMixCalculatorItem.shortLabel}</span>
|
||||
<span class="nav-icon">
|
||||
<span class="nav-icon-mask" style="--nav-icon-url: url('/icons/calculator.svg');" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span>{visibleMixCalculatorItem.label}</span>
|
||||
</a>
|
||||
{/if}
|
||||
@@ -918,16 +933,19 @@
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.sidebar :global(.sidebar-logo) {
|
||||
width: min(100%, 12.2rem);
|
||||
.nav-icon-mask {
|
||||
display: inline-block;
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
background-color: currentColor;
|
||||
-webkit-mask: var(--nav-icon-url) center / contain no-repeat;
|
||||
mask: var(--nav-icon-url) center / contain no-repeat;
|
||||
}
|
||||
|
||||
.sidebar :global(.sidebar-logo .logo-copy strong) {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.sidebar :global(.sidebar-logo .logo-mark) {
|
||||
width: 2.6rem;
|
||||
.sidebar .sidebar-logo {
|
||||
width: min(100%, 13.5rem);
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-toggle,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '$lib/api';
|
||||
import { featureFlags } from '$lib/features';
|
||||
import { clientSession, hasModuleAccess } from '$lib/session';
|
||||
import type {
|
||||
MixCalculatorCreateInput,
|
||||
@@ -169,7 +170,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSession(mode: 'update' | 'create') {
|
||||
function clearForm() {
|
||||
clientName = options.clients[0] ?? '';
|
||||
productId = 0;
|
||||
mixDate = todayIso;
|
||||
batchSizeKg = '';
|
||||
preparedByName = $clientSession?.name ?? '';
|
||||
notes = '';
|
||||
preview = null;
|
||||
formError = '';
|
||||
formSuccess = '';
|
||||
}
|
||||
|
||||
function printPreview() {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.print();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSession(mode: 'update' | 'create', destination: 'detail' | 'print' = 'detail') {
|
||||
const payload = buildPayload();
|
||||
if (!payload) {
|
||||
return;
|
||||
@@ -182,7 +201,8 @@
|
||||
? await api.updateMixCalculatorSession(initialSession.id, payload)
|
||||
: await api.createMixCalculatorSession(payload);
|
||||
|
||||
await goto(`/mix-calculator/${saved.id}`);
|
||||
const target = destination === 'print' ? `/mix-calculator/${saved.id}/print` : `/mix-calculator/${saved.id}`;
|
||||
await goto(target);
|
||||
} catch (error) {
|
||||
formError = error instanceof Error ? error.message : 'Unable to save the mix calculator session.';
|
||||
saveLoading = false;
|
||||
@@ -195,17 +215,24 @@
|
||||
<p class="eyebrow">Mix Calculator</p>
|
||||
<h2>Edit access is required to create a new session.</h2>
|
||||
<p>View-only users can open saved sessions from history, but cannot create or update production calculations.</p>
|
||||
<a class="secondary-button" href="/mix-calculator">Back to session history</a>
|
||||
{#if featureFlags.mixCalculatorSessionHistory}
|
||||
<a class="secondary-button" href="/mix-calculator">Back to session history</a>
|
||||
{/if}
|
||||
</section>
|
||||
{:else}
|
||||
<section class="page-intro">
|
||||
<div>
|
||||
<p class="eyebrow">Mix Calculator</p>
|
||||
<p class="eyebrow">
|
||||
<span class="eyebrow-icon" style="--button-icon-url: url('/icons/calculator.svg');" aria-hidden="true"></span>
|
||||
<span>Mix Calculator</span>
|
||||
</p>
|
||||
<h2>{isExistingSession ? 'Edit saved mix session' : 'New mix calculation session'}</h2>
|
||||
<p>Scale a saved product mix by batch size, review required raw materials, then save the session for history and printing.</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a class="secondary-button" href="/mix-calculator">Session history</a>
|
||||
{#if featureFlags.mixCalculatorSessionHistory}
|
||||
<a class="secondary-button" href="/mix-calculator">Session history</a>
|
||||
{/if}
|
||||
{#if initialSession}
|
||||
<a class="secondary-button" href={`/mix-calculator/${initialSession.id}/print`}>Printable view</a>
|
||||
{/if}
|
||||
@@ -288,18 +315,36 @@
|
||||
{#if canEdit}
|
||||
<div class="action-row">
|
||||
<button class="primary-button" disabled={previewLoading || saveLoading} type="button" onclick={calculatePreview}>
|
||||
{previewLoading ? 'Calculating...' : 'Calculate mix'}
|
||||
<span class="button-icon" style="--button-icon-url: url('/icons/calculator.svg');" aria-hidden="true"></span>
|
||||
<span>{previewLoading ? 'Calculating...' : 'Calculate mix'}</span>
|
||||
</button>
|
||||
|
||||
<button class="secondary-button" disabled={saveLoading || previewLoading} type="button" onclick={() => saveSession(isExistingSession ? 'update' : 'create')}>
|
||||
{saveLoading ? 'Saving...' : isExistingSession ? 'Save changes' : 'Save session'}
|
||||
</button>
|
||||
{#if featureFlags.mixCalculatorSessionSave}
|
||||
<button class="secondary-button" disabled={saveLoading || previewLoading} type="button" onclick={() => saveSession(isExistingSession ? 'update' : 'create')}>
|
||||
{saveLoading ? 'Saving...' : isExistingSession ? 'Save changes' : 'Save session'}
|
||||
</button>
|
||||
|
||||
{#if initialSession}
|
||||
<button class="secondary-button" disabled={saveLoading || previewLoading} type="button" onclick={() => saveSession('create')}>
|
||||
Save as new
|
||||
<button class="secondary-button" disabled={saveLoading || previewLoading} type="button" onclick={() => saveSession(isExistingSession ? 'update' : 'create', 'print')}>
|
||||
<span class="button-icon" style="--button-icon-url: url('/icons/print.svg');" aria-hidden="true"></span>
|
||||
<span>{saveLoading ? 'Saving...' : 'Save & print'}</span>
|
||||
</button>
|
||||
|
||||
{#if initialSession}
|
||||
<button class="secondary-button" disabled={saveLoading || previewLoading} type="button" onclick={() => saveSession('create')}>
|
||||
Save as new
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button class="secondary-button" disabled={previewLoading || saveLoading || !preview} type="button" onclick={printPreview}>
|
||||
<span class="button-icon" style="--button-icon-url: url('/icons/print.svg');" aria-hidden="true"></span>
|
||||
<span>Print</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button class="secondary-button" disabled={previewLoading || saveLoading} type="button" onclick={clearForm}>
|
||||
<span class="button-icon" style="--button-icon-url: url('/icons/trash.svg');" aria-hidden="true"></span>
|
||||
<span>Clear</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
@@ -396,6 +441,92 @@
|
||||
{/if}
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{#if preview}
|
||||
<section class="print-only" aria-hidden="true">
|
||||
<article class="print-sheet">
|
||||
<header class="print-header">
|
||||
<div>
|
||||
<p class="print-eyebrow">Mix Calculator</p>
|
||||
<h1>{preview.product_name}</h1>
|
||||
<p class="print-subtitle">{preview.client_name} · {preview.mix_name}</p>
|
||||
</div>
|
||||
<div class="print-meta">
|
||||
<div>
|
||||
<span>Mix date</span>
|
||||
<strong>{formatDate(preview.mix_date)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Prepared by</span>
|
||||
<strong>{preview.prepared_by_name}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="print-summary">
|
||||
<div>
|
||||
<span>Batch size</span>
|
||||
<strong>{formatNumber(preview.batch_size_kg, 2)}kg</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Total kilograms</span>
|
||||
<strong>{formatNumber(preview.total_kg, 2)}kg</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Total bags</span>
|
||||
<strong>{formatNumber(preview.total_bags, 2)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Unit size</span>
|
||||
<strong>{formatNumber(preview.product_unit_size_kg, 2)}kg</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if preview.notes}
|
||||
<section class="print-notes">
|
||||
<h2>Notes</h2>
|
||||
<p>{preview.notes}</p>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if preview.warnings.length}
|
||||
<section class="print-warnings">
|
||||
<h2>Warnings</h2>
|
||||
{#each preview.warnings as warning}
|
||||
<p>{warning}</p>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section class="print-table">
|
||||
<div class="print-table-header">
|
||||
<h2>Required raw materials</h2>
|
||||
<span>{preview.product_unit_of_measure} · {formatNumber(preview.product_unit_size_kg, 2)}kg per unit</span>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Raw material</th>
|
||||
<th>Mix %</th>
|
||||
<th>Required kg</th>
|
||||
<th>Unit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each preview.lines as line}
|
||||
<tr>
|
||||
<td>{line.raw_material_name}</td>
|
||||
<td>{formatNumber(line.mix_percentage, 2)}%</td>
|
||||
<td>{formatNumber(line.required_kg, 2)}kg</td>
|
||||
<td>{line.unit}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@@ -411,6 +542,18 @@
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.eyebrow-icon {
|
||||
display: inline-block;
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
background-color: currentColor;
|
||||
-webkit-mask: var(--button-icon-url) center / contain no-repeat;
|
||||
mask: var(--button-icon-url) center / contain no-repeat;
|
||||
}
|
||||
|
||||
.page-intro,
|
||||
@@ -593,6 +736,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.78rem 0.96rem;
|
||||
border-radius: 0.9rem;
|
||||
border: 1px solid var(--line-strong);
|
||||
@@ -632,6 +776,16 @@
|
||||
box-shadow: 0 10px 22px rgba(24, 38, 29, 0.08);
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-color: currentColor;
|
||||
-webkit-mask: var(--button-icon-url) center / contain no-repeat;
|
||||
mask: var(--button-icon-url) center / contain no-repeat;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.7;
|
||||
@@ -781,4 +935,151 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.print-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
:global(body) {
|
||||
background: #fff !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
:global(body *) {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.print-only,
|
||||
.print-only :global(*) {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.print-only {
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
padding: 1.4cm;
|
||||
background: #fff;
|
||||
color: #1a2421;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.print-sheet {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.print-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #cbd6cf;
|
||||
}
|
||||
|
||||
.print-header h1 {
|
||||
margin: 0.25rem 0 0.3rem;
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
|
||||
.print-eyebrow {
|
||||
color: #5f6f67;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.print-subtitle {
|
||||
color: #5f6f67;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.print-meta {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
min-width: 12rem;
|
||||
}
|
||||
|
||||
.print-meta div,
|
||||
.print-summary div {
|
||||
display: grid;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.print-meta span,
|
||||
.print-summary span,
|
||||
.print-table-header span {
|
||||
color: #5f6f67;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.print-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.85rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid #cbd6cf;
|
||||
}
|
||||
|
||||
.print-notes,
|
||||
.print-warnings {
|
||||
margin-top: 0.85rem;
|
||||
padding: 0.75rem 0.9rem;
|
||||
border: 1px solid #cbd6cf;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.print-warnings {
|
||||
border-color: #d8a76b;
|
||||
background: #fff6e6;
|
||||
color: #8b5b1e;
|
||||
}
|
||||
|
||||
.print-notes h2,
|
||||
.print-warnings h2,
|
||||
.print-table-header h2 {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.print-table {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.print-table-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.print-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.print-table th,
|
||||
.print-table td {
|
||||
padding: 0.55rem 0.5rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #cbd6cf;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.print-table th {
|
||||
color: #5f6f67;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 1cm;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { env } from '$env/dynamic/public';
|
||||
|
||||
function flagEnabled(value: string | undefined, defaultValue: boolean): boolean {
|
||||
if (value === undefined || value === null || value.trim() === '') {
|
||||
return defaultValue;
|
||||
}
|
||||
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
||||
}
|
||||
|
||||
export const featureFlags = {
|
||||
mixCalculatorSessionHistory: flagEnabled(env.PUBLIC_MIX_CALCULATOR_SESSION_HISTORY, false),
|
||||
mixCalculatorSessionSave: flagEnabled(env.PUBLIC_MIX_CALCULATOR_SESSION_SAVE, false)
|
||||
};
|
||||
Reference in New Issue
Block a user