groceries: mobile-first tracker redesign — compact rows per product group (cheapest store badge, unit price, spread, promo), tap to compare stores; category tree (23 tops, subcategories, icons/colours) with chips, store tiles, sort and non-food toggle; owner shelf prices (manual source with date, photo, note)
Some checks failed
Deploy production / deploy (push) Failing after 1m1s

This commit is contained in:
2026-10-25 19:02:44 +00:00
parent 67d74f39b8
commit 707f07efd4
13 changed files with 33141 additions and 447 deletions

View File

@@ -1,5 +1,14 @@
import { describe, expect, it } from 'vitest';
import { cheapest, formatAmount, formatUnitPrice, sortByPrice, type PricedItem } from './groceries';
import {
bestPerStore,
cheapest,
formatAmount,
formatUnitPrice,
parseEuros,
sortByPrice,
toOffers,
type PricedItem
} from './groceries';
const it_ = (
id: number,
@@ -42,3 +51,69 @@ describe('grocery price helpers', () => {
expect(formatUnitPrice(null, 'kg')).toBeNull();
});
});
describe('parseEuros', () => {
it('reads Dutch and English prices', () => {
expect(parseEuros('1,29')).toBe(129);
expect(parseEuros('€ 2.5')).toBe(250);
expect(parseEuros('3')).toBe(300);
});
it('rejects nonsense', () => {
expect(parseEuros('')).toBeNull();
expect(parseEuros('0')).toBeNull();
expect(parseEuros('1,299')).toBeNull();
expect(parseEuros('abc')).toBeNull();
});
});
describe('bestPerStore', () => {
const item = (
id: number,
storeId: number,
price: number,
unit: number | null,
promo?: number
) => ({
id,
storeId,
name: `p${id}`,
sizeText: null,
unit: 'kg',
unitAmount: 1,
promoLabel: promo ? '1+1' : null,
url: null,
available: true,
priceCents: price,
promoPriceCents: promo ?? null,
unitPriceCents: unit,
promoUnitPriceCents: promo ?? null
});
it('keeps the cheapest offer per store, cheapest store first, with the spread', () => {
const offers = toOffers(
[item(1, 1, 300, 300), item(2, 1, 250, 250), item(3, 2, 400, 400, 200), item(4, 3, 500, 500)],
[
{
id: 9,
itemId: null,
storeId: 4,
priceCents: 180,
promo: false,
sizeText: '1 kg',
unit: 'kg',
unitPriceCents: 180,
observedOn: '2026-10-20',
photoUrl: null,
note: null
}
]
);
const r = bestPerStore(offers, 'unit');
expect(r.stores.map((o) => o.key)).toEqual(['m9', 'i3', 'i2', 'i4']);
expect(r.best?.manual?.id).toBe(9);
expect(r.stores[1].regularCents).toBe(400);
expect(r.spread).toEqual({ min: 180, max: 500, pct: 178 });
});
it('has no spread for one store', () => {
expect(bestPerStore(toOffers([item(1, 1, 100, 100)], []), 'pack').spread).toBeNull();
});
});