This commit is contained in:
2026-06-09 21:28:53 +12:00
parent daa6e60a69
commit 349e4a4b5b
61 changed files with 6404 additions and 1382 deletions
+471 -17
View File
@@ -6,7 +6,18 @@
import type { DashboardSummary } from '$lib/types';
import { canOpenEditor, getWorkspaceHomeHref } from '$lib/workspace-access';
import packageInfo from '../../package.json';
import { Sunrise, Sun, Sunset, Moon } from 'lucide-svelte';
import {
ArrowUpRight,
BadgeDollarSign,
Factory,
PackageCheck,
Scale,
Sun,
Sunrise,
Sunset,
TriangleAlert,
Moon
} from 'lucide-svelte';
import { tick } from 'svelte';
type Segment = {
@@ -115,6 +126,13 @@
return `$${value.toFixed(digits)}`;
}
function kg(value: number | null | undefined) {
if (value === null || value === undefined) {
return '0 kg';
}
return `${value.toLocaleString(undefined, { maximumFractionDigits: 0 })} kg`;
}
function formatDate(value: string | null | undefined) {
if (!value) {
return 'No date';
@@ -314,6 +332,7 @@
const trendArea = $derived(areaPath(trendSeries));
const trendFocus = $derived(focusMarker(trendSeries));
const topProducts = $derived(summary?.products?.top_products ?? []);
const operations = $derived(summary?.operations ?? null);
const focusCards = $derived(buildFocusCards(summary));
const loading = $derived(summary === null);
const greeting = $derived(timeOfDay());
@@ -465,7 +484,7 @@
</div>
<div class="intro-actions">
{#if canOpenEditor($clientSession)}
<a class="primary-button" href="/editor">Open Editor</a>
<a class="primary-button" href="/editor">Open Mix Editor</a>
{/if}
</div>
</section>
@@ -610,6 +629,146 @@
</div>
</section>
<section class="operations-report">
<div class="card-toolbar operations-toolbar">
<div class="operations-heading">
<span class="operations-icon"><Factory size={24} strokeWidth={2.2} /></span>
<div>
<h3>Production And Pricing</h3>
<p>Throughput and Product Costing for {operations?.period_label ?? 'this month'}</p>
</div>
</div>
<a class="secondary-button compact operations-link" href="/product-costing">
Open Product Costing
<ArrowUpRight size={15} strokeWidth={2.3} />
</a>
</div>
<div class="operations-graphic" aria-hidden="true">
<div class="operations-graphic-labels">
<span>Throughput</span>
<span>Costing</span>
<span>Pricing</span>
</div>
<div class="operations-graphic-track">
<span class="operation-node production-node"></span>
<span class="operation-track"></span>
<span class="operation-bars">
<i></i>
<i></i>
<i></i>
</span>
<span class="operation-node pricing-node"></span>
</div>
</div>
<div class="operations-metrics">
<article class="produced">
<div class="metric-label">
<span>Produced</span>
<span class="metric-symbol"><PackageCheck size={17} strokeWidth={2.2} /></span>
</div>
{#if loading}
<strong><Skeleton width="6rem" height="1.5rem" /></strong>
{:else}
<strong>{kg(operations?.total_kg)}</strong>
{/if}
<p>{operations?.entry_count ?? 0} throughput entries</p>
</article>
<article class="bags">
<div class="metric-label">
<span>Bags</span>
<span class="metric-symbol"><Scale size={17} strokeWidth={2.2} /></span>
</div>
{#if loading}
<strong><Skeleton width="5rem" height="1.5rem" /></strong>
{:else}
<strong>{(operations?.total_bags ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}</strong>
{/if}
<p>Logged as bag runs</p>
</article>
<article class="value">
<div class="metric-label">
<span>Wholesale Value</span>
<span class="metric-symbol"><BadgeDollarSign size={17} strokeWidth={2.2} /></span>
</div>
{#if loading}
<strong><Skeleton width="7rem" height="1.5rem" /></strong>
{:else}
<strong>{currency(operations?.estimated_wholesale_value)}</strong>
{/if}
<p>{operations?.priced_entry_count ?? 0} priced entries</p>
</article>
<article class:warning={(operations?.pricing_issues?.total ?? 0) > 0}>
<div class="metric-label">
<span>Pricing Issues</span>
<span class="metric-symbol"><TriangleAlert size={17} strokeWidth={2.2} /></span>
</div>
{#if loading}
<strong><Skeleton width="4rem" height="1.5rem" /></strong>
{:else}
<strong>{operations?.pricing_issues?.total ?? 0}</strong>
{/if}
<p>Products needing review</p>
</article>
</div>
<div class="operations-grid">
<article>
<div class="mini-heading">
<strong>Top Produced</strong>
<span>By kg</span>
</div>
<div class="report-list">
{#if loading}
{#each Array(4) as _}
<div><Skeleton width="10rem" /><Skeleton width="4rem" /></div>
{/each}
{:else if operations?.top_products?.length}
{#each operations.top_products as product}
<div>
<span>
<strong>{product.product_name}</strong>
<small>{product.client_name ?? 'No client'} · {product.entries} entries</small>
</span>
<em>{kg(product.kg)}</em>
</div>
{/each}
{:else}
<p>No throughput recorded this month.</p>
{/if}
</div>
</article>
<article>
<div class="mini-heading">
<strong>Produced But Not Priced</strong>
<span>Fix these first</span>
</div>
<div class="report-list">
{#if loading}
{#each Array(4) as _}
<div><Skeleton width="10rem" /><Skeleton width="4rem" /></div>
{/each}
{:else if operations?.produced_not_priced?.length}
{#each operations.produced_not_priced as product}
<div>
<span>
<strong>{product.product_name}</strong>
<small>{product.warnings[0] ?? product.status}</small>
</span>
<em>{kg(product.kg)}</em>
</div>
{/each}
{:else}
<p>All produced products have usable pricing.</p>
{/if}
</div>
</article>
</div>
</section>
<section class="analysis-grid">
<article class="panel-card chart-card">
<div class="card-toolbar">
@@ -1094,6 +1253,7 @@
.workspace-banner,
.focus-row,
.dashboard-grid,
.operations-report,
.analysis-grid,
.detail-grid {
margin-bottom: 1.25rem;
@@ -1177,15 +1337,15 @@
.primary-button {
border: none;
color: #fff;
color: var(--color-on-brand);
background: var(--color-brand);
box-shadow: none;
}
.secondary-button {
border: 1px solid var(--line-strong);
color: #304038;
background: #fff;
border: 1px solid var(--color-border);
color: var(--color-text-primary);
background: var(--color-bg-surface);
}
.secondary-button.compact {
@@ -1206,6 +1366,276 @@
padding: 1.2rem;
}
.operations-report {
position: relative;
overflow: hidden;
padding: 1.2rem;
border: 1px solid var(--color-border);
border-radius: 1.4rem;
background: var(--color-bg-surface);
box-shadow: var(--shadow);
}
.operations-toolbar {
margin-bottom: 0.9rem;
}
.operations-heading {
display: flex;
align-items: center;
gap: 0.85rem;
min-width: 0;
}
.operations-heading h3 {
margin: 0 0 0.18rem;
color: var(--text);
}
.operations-heading p {
margin: 0;
color: var(--muted);
}
.operations-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3.1rem;
height: 3.1rem;
flex-shrink: 0;
border: 1px solid rgba(21, 128, 61, 0.18);
border-radius: 1rem;
background: #f0f8f3;
color: #0f6f3d;
box-shadow: inset 0 -0.45rem 1rem rgba(21, 128, 61, 0.08);
}
.operations-graphic {
display: grid;
gap: 0.5rem;
margin-bottom: 0.95rem;
padding: 0.75rem 0.85rem;
border: 1px solid rgba(214, 228, 220, 0.86);
border-radius: 1rem;
background: rgba(255, 255, 255, 0.62);
box-shadow: inset 0 -0.6rem 1.4rem rgba(21, 128, 61, 0.06);
}
.operations-graphic-labels,
.operations-graphic-track {
display: grid;
grid-template-columns: 2.25rem minmax(4rem, 1fr) 4.5rem 2.25rem;
align-items: center;
gap: 0.45rem;
}
.operations-graphic-labels {
grid-template-columns: repeat(3, minmax(0, 1fr));
color: #365243;
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.operations-graphic-labels span:nth-child(2) {
text-align: center;
}
.operations-graphic-labels span:nth-child(3) {
text-align: right;
}
.operation-node {
display: block;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.8rem;
background: #0f6f3d;
box-shadow: inset 0 -0.35rem 0.65rem rgba(0, 0, 0, 0.12);
}
.pricing-node {
background: #e7ad3c;
}
.operation-track {
align-self: center;
height: 0.24rem;
border-radius: 999px;
background: linear-gradient(90deg, rgba(21, 128, 61, 0.22), rgba(59, 130, 196, 0.58), rgba(231, 173, 60, 0.55));
}
.operation-bars {
display: flex;
align-items: end;
justify-content: center;
gap: 0.28rem;
height: 2.4rem;
}
.operation-bars i {
display: block;
width: 0.7rem;
border-radius: 999px 999px 0.25rem 0.25rem;
background: #3b82c4;
}
.operation-bars i:nth-child(1) {
height: 1.2rem;
}
.operation-bars i:nth-child(2) {
height: 2rem;
background: #15803d;
}
.operation-bars i:nth-child(3) {
height: 1.55rem;
background: #e7ad3c;
}
.operations-link {
gap: 0.45rem;
white-space: nowrap;
}
.operations-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.8rem;
margin-bottom: 1rem;
}
.operations-metrics article {
position: relative;
overflow: hidden;
padding: 0.95rem;
border: 1px solid rgba(214, 228, 220, 0.94);
border-radius: 1rem;
background: rgba(255, 255, 255, 0.84);
box-shadow: 0 0.75rem 1.4rem rgba(43, 57, 47, 0.05);
}
.operations-metrics article::after {
content: '';
position: absolute;
right: 0.85rem;
bottom: 0;
left: 0.85rem;
height: 0.24rem;
border-radius: 999px 999px 0 0;
background: #15803d;
opacity: 0.65;
}
.operations-metrics article.bags::after {
background: #3b82c4;
}
.operations-metrics article.value::after {
background: #e7ad3c;
}
.operations-metrics article.warning {
border-color: rgba(231, 173, 60, 0.44);
background: #fff8e8;
}
.operations-metrics article.warning::after {
background: #b45309;
}
.metric-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.65rem;
}
.metric-symbol {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #0f6f3d;
}
.bags .metric-symbol {
color: #2f6f9f;
}
.value .metric-symbol {
color: #9a6718;
}
.warning .metric-symbol {
color: #9a3412;
}
.metric-label span,
.operations-metrics p,
.mini-heading span,
.report-list small {
color: var(--muted);
}
.operations-metrics strong {
display: block;
margin: 0.4rem 0 0.25rem;
font-size: 1.55rem;
line-height: 1;
}
.operations-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.operations-grid > article {
padding-top: 0.9rem;
border-top: 1px solid var(--line);
}
.mini-heading,
.report-list div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
}
.mini-heading {
margin-bottom: 0.65rem;
}
.report-list {
display: grid;
gap: 0.55rem;
}
.report-list div {
padding: 0.65rem 0;
border-bottom: 1px solid var(--line);
}
.report-list div:last-child {
border-bottom: none;
}
.report-list strong,
.report-list small {
display: block;
}
.report-list em {
flex-shrink: 0;
font-style: normal;
font-weight: 700;
}
.focus-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -1249,15 +1679,15 @@
}
.focus-card.positive {
background: linear-gradient(180deg, #f6fbf7 0%, #edf8f0 100%);
background: var(--color-success-tint);
}
.focus-card.warning {
background: linear-gradient(180deg, #fffaf3 0%, #fff3e3 100%);
background: var(--color-warning-tint);
}
.focus-card.neutral {
background: linear-gradient(180deg, #f7faf8 0%, #eff4f1 100%);
background: var(--panel-soft);
}
.focus-code {
@@ -1267,8 +1697,8 @@
align-items: center;
justify-content: center;
border-radius: 0.75rem;
color: #fff;
background: var(--green-deep);
color: var(--color-on-brand);
background: var(--color-brand);
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.05em;
@@ -1369,8 +1799,9 @@
}
.toggle-pill .active {
color: #fff;
background: var(--green-deep);
color: var(--color-text-primary);
background: var(--color-bg-surface);
border: 1px solid var(--color-border);
}
.market-layout {
@@ -1410,7 +1841,11 @@
height: 8.5rem;
flex-shrink: 0;
border-radius: 1.4rem;
background: linear-gradient(180deg, #d7f0ff 0%, #fff0bf 45%, #89c762 46%, #3f8e3d 100%);
background: linear-gradient(
150deg,
var(--color-brand) 0%,
color-mix(in srgb, var(--color-brand) 70%, var(--color-text-primary)) 100%
);
overflow: hidden;
}
@@ -1421,7 +1856,12 @@
width: 2.9rem;
height: 2.9rem;
border-radius: 999px;
background: radial-gradient(circle at 35% 35%, #fff8cc 0%, #ffd865 58%, #f4ae1f 100%);
background: radial-gradient(
circle at 38% 38%,
color-mix(in srgb, var(--color-on-brand) 38%, transparent) 0%,
color-mix(in srgb, var(--color-on-brand) 12%, transparent) 62%,
transparent 72%
);
}
.field-stripe {
@@ -1430,7 +1870,7 @@
right: -10%;
height: 22%;
border-radius: 999px;
background: rgba(255, 255, 255, 0.18);
background: color-mix(in srgb, var(--color-on-brand) 16%, transparent);
transform: rotate(-18deg);
}
@@ -1864,10 +2304,15 @@
@media (max-width: 1120px) {
.analysis-grid,
.detail-grid {
.detail-grid,
.operations-grid {
grid-template-columns: 1fr;
}
.operations-link {
justify-self: start;
}
.focus-grid {
min-width: 0;
}
@@ -1889,6 +2334,10 @@
.focus-grid {
grid-template-columns: 1fr;
}
.operations-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (prefers-reduced-motion: reduce) {
@@ -1935,10 +2384,15 @@
}
.preview-facts,
.operations-metrics,
.signin-form {
grid-template-columns: 1fr;
}
.operations-graphic {
width: 100%;
}
.field-emblem {
width: 6.5rem;
height: 6.5rem;