groceries stats: fix array/json params for the app client (live store filter 500), CPI axis, header spacing
All checks were successful
Deploy production / deploy (push) Successful in 1m11s

This commit is contained in:
2026-10-25 20:48:00 +00:00
parent 8e35a58095
commit 6dcbcfee25
2 changed files with 14 additions and 7 deletions

View File

@@ -41,6 +41,8 @@ type Tx = TransactionSql;
const ALL = 'all'; const ALL = 'all';
// Leaves under food tops that are not edible (the taxonomy flags them food via their top). // Leaves under food tops that are not edible (the taxonomy flags them food via their top).
const NON_FOOD_LEAVES = ['baby-kind--luiers', 'baby-kind--verzorging', 'baby-kind--accessoires']; const NON_FOOD_LEAVES = ['baby-kind--luiers', 'baby-kind--verzorging', 'baby-kind--accessoires'];
// Passed as an array literal: the app's postgres client doesn't serialise JS arrays here.
const NON_FOOD_ARR = `{${NON_FOOD_LEAVES.join(',')}}`;
// Own (private-label) brands per store, as a regex on lower(brand). Superunie labels are shared // Own (private-label) brands per store, as a regex on lower(brand). Superunie labels are shared
// by its members (Dirk, Hoogvliet, Vomar, PLUS). `unbranded`: items without a brand are the // by its members (Dirk, Hoogvliet, Vomar, PLUS). `unbranded`: items without a brand are the
@@ -327,10 +329,10 @@ async function prepare(tx: Tx, scope: StatsScope, stores: number[] | null) {
left join groceries.groups g on g.id = i.group_id left join groceries.groups g on g.id = i.group_id
left join groceries.categories c on c.id = i.category_id left join groceries.categories c on c.id = i.category_id
left join groceries.categories tc on tc.id = coalesce(c.parent_id, c.id) left join groceries.categories tc on tc.id = coalesce(c.parent_id, c.id)
left join json_to_recordset(${tx.json(OWN_BRANDS as never)}::json) left join json_to_recordset(${JSON.stringify(OWN_BRANDS)}::text::json)
as ob(slug text, rx text, unbranded boolean) on ob.slug = s.slug as ob(slug text, rx text, unbranded boolean) on ob.slug = s.slug
where i.available and i.price_cents > 0 and i.unit_price_cents > 0 and i.unit is not null where i.available and i.price_cents > 0 and i.unit_price_cents > 0 and i.unit is not null
and (${scope} = 'all' or (i.is_food and coalesce(c.slug, '') <> all(${NON_FOOD_LEAVES}) and (${scope} = 'all' or (i.is_food and coalesce(c.slug, '') <> all(${NON_FOOD_ARR}::text[])
and i.name !~* 'e-?gift|cadeaukaart|giftcard|gift card')) and i.name !~* 'e-?gift|cadeaukaart|giftcard|gift card'))
and (${storeArr}::int[] is null or i.store_id = any(${storeArr}::int[])) and (${storeArr}::int[] is null or i.store_id = any(${storeArr}::int[]))
and i.unit_price_cents >= case i.unit when 'piece' then 1 else 10 end and i.unit_price_cents >= case i.unit when 'piece' then 1 else 10 end
@@ -717,7 +719,7 @@ async function history(tx: Tx, scope: StatsScope, stores: number[] | null) {
join groceries.stores s on s.id = i.store_id and s.archived_at is null join groceries.stores s on s.id = i.store_id and s.archived_at is null
left join groceries.categories c on c.id = i.category_id left join groceries.categories c on c.id = i.category_id
left join groceries.categories tc on tc.id = coalesce(c.parent_id, c.id) left join groceries.categories tc on tc.id = coalesce(c.parent_id, c.id)
where (${scope} = 'all' or (i.is_food and coalesce(c.slug, '') <> all(${NON_FOOD_LEAVES}) where (${scope} = 'all' or (i.is_food and coalesce(c.slug, '') <> all(${NON_FOOD_ARR}::text[])
and i.name !~* 'e-?gift|cadeaukaart|giftcard|gift card')) and i.name !~* 'e-?gift|cadeaukaart|giftcard|gift card'))
and (${storeArr}::int[] is null or i.store_id = any(${storeArr}::int[]))`; and (${storeArr}::int[] is null or i.store_id = any(${storeArr}::int[]))`;
await tx`create unique index on it (id)`; await tx`create unique index on it (id)`;
@@ -904,7 +906,7 @@ async function history(tx: Tx, scope: StatsScope, stores: number[] | null) {
async function basket(tx: Tx) { async function basket(tx: Tx) {
const rows = await tx` const rows = await tx`
with st as ( with st as (
select * from json_to_recordset(${tx.json(STAPLES as never)}::json) select * from json_to_recordset(${JSON.stringify(STAPLES)}::text::json)
as x(key text, rx text, nrx text, units text, qty float8, cats text, min float8, max float8, as x(key text, rx text, nrx text, units text, qty float8, cats text, min float8, max float8,
"pieceKg" float8) "pieceKg" float8)
), ),

View File

@@ -394,7 +394,12 @@
tooltip: { trigger: 'axis', valueFormatter: (v: number) => (v == null ? '–' : v.toFixed(2)) }, tooltip: { trigger: 'axis', valueFormatter: (v: number) => (v == null ? '–' : v.toFixed(2)) },
legend: legend(t), legend: legend(t),
xAxis: { type: 'category', data: (s?.days ?? []).map(fmtDay), axisLabel: { color: t.muted } }, xAxis: { type: 'category', data: (s?.days ?? []).map(fmtDay), axisLabel: { color: t.muted } },
yAxis: axisVal(t), yAxis: {
...axisVal(t),
scale: true,
minInterval: 0.1,
axisLabel: { color: t.muted, formatter: (v: number) => v.toFixed(1) }
},
series: ids.map((k) => ({ series: ids.map((k) => ({
name: sname(Number(k)), name: sname(Number(k)),
type: 'line', type: 'line',
@@ -588,8 +593,8 @@
<p class="mt-2 flex flex-wrap items-center gap-1.5 text-sm text-[var(--site-muted)]"> <p class="mt-2 flex flex-wrap items-center gap-1.5 text-sm text-[var(--site-muted)]">
<Lock size={14} aria-hidden="true" /> <Lock size={14} aria-hidden="true" />
{#if ready && meta} {#if ready && meta}
Snapshot of {fmtDay(ready.day)} · {historyDays} day{historyDays === 1 ? '' : 's'} of price history{#if ready.durationMs} Snapshot of {fmtDay(ready.day)} · {historyDays} day{historyDays === 1 ? '' : 's'} of price history
· computed in {(ready.durationMs / 1000).toFixed(1)} s{/if} {#if ready.durationMs}· computed in {(ready.durationMs / 1000).toFixed(1)} s{/if}
{:else}Who's cheapest, how often, and by how much.{/if} {:else}Who's cheapest, how often, and by how much.{/if}
</p> </p>
</div> </div>