v0.1.30 - Throughput overview today-only mix cards; mix formula save 500 fix
Throughput Overview: Horse Mix and Grain Mix are now the first two cards and
show TODAY's output only. Removed the 7d/4w/6w/12w range selector; the cards
are fixed to Horse mix today, Grain mix today, Today, This week, 4-week average.
Mix Editor formula save: fix HTTP 500 on PUT /editor/mixes/{id}/formula. The
audit-diff path read the resolved formula by attribute, but the resolver returns
dicts -> AttributeError. Read by key and expire stale ORM state so the response
reflects the just-saved rows. Adds regression tests for both save branches.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hunter-app",
|
||||
"version": "0.1.29",
|
||||
"version": "0.1.30",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hunter-app",
|
||||
"version": "0.1.29",
|
||||
"version": "0.1.30",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"lucide-svelte": "^1.0.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hunter-app",
|
||||
"version": "0.1.29",
|
||||
"version": "0.1.30",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { CalendarDays, CalendarRange, Carrot, Gauge, TrendingUp, Wheat } from 'lucide-svelte';
|
||||
|
||||
import { MIX_RANGES } from '$lib/components/throughput/utils';
|
||||
|
||||
let {
|
||||
today,
|
||||
weekRangeLabel,
|
||||
heroStats,
|
||||
mixTotals,
|
||||
mixRangeKey = $bindable<(typeof MIX_RANGES)[number]['key']>('4w'),
|
||||
formatDate,
|
||||
formatNumber
|
||||
}: {
|
||||
@@ -16,7 +13,6 @@
|
||||
weekRangeLabel: string;
|
||||
heroStats: { today: number; thisWeek: number; avgFourWeek: number };
|
||||
mixTotals: { horse: number; grain: number };
|
||||
mixRangeKey?: (typeof MIX_RANGES)[number]['key'];
|
||||
formatDate: (value: string) => string;
|
||||
formatNumber: (value: number | null | undefined, digits?: number) => string;
|
||||
} = $props();
|
||||
@@ -26,18 +22,19 @@
|
||||
<div class="summary-heading">
|
||||
<span class="summary-icon"><Gauge size={17} strokeWidth={2.2} /></span>
|
||||
<h2>Throughput Overview</h2>
|
||||
<div class="range-select" role="group" aria-label="Customer mix date range">
|
||||
{#each MIX_RANGES as range (range.key)}
|
||||
<button
|
||||
type="button"
|
||||
class="range-option"
|
||||
class:active={mixRangeKey === range.key}
|
||||
aria-pressed={mixRangeKey === range.key}
|
||||
onclick={() => (mixRangeKey = range.key)}
|
||||
>{range.label}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<dl class="mix-facts" aria-label="Throughput by customer today">
|
||||
<div class="fact">
|
||||
<dt><span class="fact-icon"><Carrot size={16} strokeWidth={2.2} /></span>Horse Mix</dt>
|
||||
<dd>{formatNumber(mixTotals.horse)} <span class="fact-unit">kg</span></dd>
|
||||
<p class="fact-sub">PHF Horsemix · {formatDate(today)}</p>
|
||||
</div>
|
||||
<div class="fact">
|
||||
<dt><span class="fact-icon"><Wheat size={16} strokeWidth={2.2} /></span>Grain Mix</dt>
|
||||
<dd>{formatNumber(mixTotals.grain)} <span class="fact-unit">kg</span></dd>
|
||||
<p class="fact-sub">All other customers · {formatDate(today)}</p>
|
||||
</div>
|
||||
</dl>
|
||||
<dl class="facts">
|
||||
<div class="fact">
|
||||
<dt><span class="fact-icon"><CalendarDays size={16} strokeWidth={2.2} /></span>Today</dt>
|
||||
@@ -55,18 +52,6 @@
|
||||
<p class="fact-sub">Per week, last 4 weeks</p>
|
||||
</div>
|
||||
</dl>
|
||||
<dl class="mix-facts" aria-label="Throughput by customer">
|
||||
<div class="fact">
|
||||
<dt><span class="fact-icon"><Carrot size={16} strokeWidth={2.2} /></span>Horse Mix</dt>
|
||||
<dd>{formatNumber(mixTotals.horse)} <span class="fact-unit">kg</span></dd>
|
||||
<p class="fact-sub">PHF Horsemix · {MIX_RANGES.find((r) => r.key === mixRangeKey)?.sub ?? 'last 4 weeks'}</p>
|
||||
</div>
|
||||
<div class="fact">
|
||||
<dt><span class="fact-icon"><Wheat size={16} strokeWidth={2.2} /></span>Grain Mix</dt>
|
||||
<dd>{formatNumber(mixTotals.grain)} <span class="fact-unit">kg</span></dd>
|
||||
<p class="fact-sub">All other customers · {MIX_RANGES.find((r) => r.key === mixRangeKey)?.sub ?? 'last 4 weeks'}</p>
|
||||
</div>
|
||||
</dl>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
@@ -106,41 +91,6 @@
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.range-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
margin-left: auto;
|
||||
padding: 0.28rem;
|
||||
border: 1px solid color-mix(in srgb, var(--color-brand) 14%, var(--color-border));
|
||||
border-radius: 0.8rem;
|
||||
background: color-mix(in srgb, var(--color-bg-surface) 55%, transparent);
|
||||
}
|
||||
|
||||
.range-option {
|
||||
padding: 0.5rem 0.95rem;
|
||||
border: 0;
|
||||
border-radius: 0.6rem;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: background-color 140ms ease, color 140ms ease, box-shadow 140ms ease;
|
||||
}
|
||||
|
||||
.range-option:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.range-option.active {
|
||||
background: var(--color-brand);
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 18px -14px color-mix(in srgb, var(--color-brand) 85%, transparent);
|
||||
}
|
||||
|
||||
.facts,
|
||||
.mix-facts {
|
||||
display: grid;
|
||||
@@ -149,8 +99,13 @@
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Mix cards lead the overview, so they carry the top padding; the hero
|
||||
figures follow and hug up against them. */
|
||||
.mix-facts {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.facts {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
@@ -243,12 +198,12 @@
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.facts {
|
||||
.mix-facts {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
.mix-facts {
|
||||
.facts {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0 0.9rem 0.9rem;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,6 @@ export type ConfettiPiece = {
|
||||
|
||||
export const CONFETTI_COLORS = ['#16a34a', '#f59e0b', '#3b82f6', '#ec4899', '#8b5cf6', '#ef4444'];
|
||||
|
||||
export const MIX_RANGES = [
|
||||
{ key: '7d', label: '7 days', sub: 'last 7 days', days: 7 },
|
||||
{ key: '4w', label: '4 weeks', sub: 'last 4 weeks', days: 28 },
|
||||
{ key: '6w', label: '6 weeks', sub: 'last 6 weeks', days: 42 },
|
||||
{ key: '12w', label: '12 weeks', sub: 'last 12 weeks', days: 84 }
|
||||
] as const;
|
||||
|
||||
export function compareText(a: string | null | undefined, b: string | null | undefined) {
|
||||
return (a ?? '').localeCompare(b ?? '', undefined, { sensitivity: 'base' });
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import ThroughputSuccessOverlay from '$lib/components/throughput/ThroughputSuccessOverlay.svelte';
|
||||
import ThroughputSummary from '$lib/components/throughput/ThroughputSummary.svelte';
|
||||
import {
|
||||
MIX_RANGES,
|
||||
addDays,
|
||||
ausToday,
|
||||
buildConfetti,
|
||||
@@ -431,15 +430,13 @@
|
||||
return norm.includes('phf') && norm.includes('horse');
|
||||
}
|
||||
|
||||
let mixRangeKey = $state<(typeof MIX_RANGES)[number]['key']>('4w');
|
||||
const mixRange = $derived(MIX_RANGES.find((r) => r.key === mixRangeKey) ?? MIX_RANGES[1]);
|
||||
|
||||
// Today's split only: Horse Mix (PHF Horsemix) vs Grain Mix (everyone else).
|
||||
const mixTotals = $derived.by(() => {
|
||||
const cutoff = toISODate(addDays(ausToday(), -(mixRange.days - 1)));
|
||||
const todayStr = toISODate(ausToday());
|
||||
let horse = 0;
|
||||
let grain = 0;
|
||||
for (const entry of statsEntries) {
|
||||
if (entry.production_date < cutoff) continue;
|
||||
if (entry.production_date !== todayStr) continue;
|
||||
const kg = entry.calculated_kg || 0;
|
||||
const client = entry.product_id != null ? productClientById.get(entry.product_id) ?? null : null;
|
||||
if (isHorseMixClient(client)) horse += kg;
|
||||
@@ -531,7 +528,6 @@
|
||||
{weekRangeLabel}
|
||||
{heroStats}
|
||||
{mixTotals}
|
||||
bind:mixRangeKey
|
||||
{formatDate}
|
||||
{formatNumber}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user