v0.1.12
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user