mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Replace the DB-backed inventory module with a config-only design and build out food-storage tooling around it. Inventory (config-only, no database) - Each inventory is a YAML file at config/inventory/<slug>.yaml, loaded once into memory and the single source of truth. New reusable MultiFileConfig (wrolpi/common.py) manages a directory of per-entity files. - Inventory types (food/fuel/tool) seed a customizable field schema (text/number/quantity/date/select/location/calories). Unit math/aggregation moved to the frontend (mathjs); Pint dropped from the backend. - Minimal API: GET / (all inventories full), POST /, PUT /<slug> (whole- inventory save with version-based optimistic concurrency; 404 for missing), DELETE. Legacy inventories.yaml / old tables migrate to per-inventory files (pound->lb), and an alembic migration drops the tables. Frontend - New app/src/components/inventory/ package: spreadsheet-style table with Tab/Enter keyboard entry, sortable columns, expired-row highlighting, customizable field editor, and pooled cross-inventory location autocomplete. - A single useInventories store loads everything once and persists whole inventories optimistically; list/items/summary/locations derive client-side. - Summary groups items (default by Category), totals size x count, shows a calories column, and embeds a ration estimate (how long the stock lasts). Food catalog - Shared, editable catalog (config/inventory/catalog.yaml) seeded from catalog_defaults.yaml (~60 generic items, total calories per package) with merge-keep-edits + tombstones. GET/PUT /api/inventory/catalog, a management modal, and entry-row pre-fill of category/size/calories from a name match. Calculators - Generic Food Storage planner (like the Water calculator): household + editable per-person/year amounts -> total to store over a chosen duration, with women/children weighted by DGA calorie shares and an imperial/metric toggle. Also fixes ConfigFile.save() to accept nested file_names. Backend pytest and frontend jest suites updated/added throughout; alembic check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.4 KiB
JavaScript
69 lines
2.4 KiB
JavaScript
import {convert, formatTotals, sumQuantities} from "./units";
|
|
|
|
describe('convert', () => {
|
|
test('converts within mass', () => {
|
|
expect(convert(16, 'oz', 'lb')).toBeCloseTo(1, 5);
|
|
expect(convert(2000, 'lb', 'ton')).toBeCloseTo(1, 5);
|
|
});
|
|
|
|
test('converts within volume', () => {
|
|
expect(convert(1, 'gallon', 'quart')).toBeCloseTo(4, 5);
|
|
});
|
|
|
|
test('returns null for incompatible or unparseable units', () => {
|
|
expect(convert(1, 'lb', 'gallon')).toBeNull();
|
|
expect(convert(1, 'each', 'lb')).toBeNull();
|
|
expect(convert('', 'lb', 'oz')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('sumQuantities', () => {
|
|
test('sums compatible units and compacts', () => {
|
|
const result = sumQuantities([
|
|
{value: '16', unit: 'oz'},
|
|
{value: '1', unit: 'lb'},
|
|
]);
|
|
expect(result.totals).toHaveLength(1);
|
|
expect(result.totals[0].unit).toBe('lb');
|
|
expect(result.totals[0].magnitude).toBeCloseTo(2, 5);
|
|
expect(result.unitlessCount).toBeNull();
|
|
});
|
|
|
|
test('compacts pounds to tons past the threshold', () => {
|
|
const result = sumQuantities([{value: '2000', unit: 'lb'}]);
|
|
expect(result.totals[0].unit).toBe('ton');
|
|
expect(result.totals[0].magnitude).toBeCloseTo(1, 5);
|
|
});
|
|
|
|
test('keeps incompatible dimensions in separate buckets', () => {
|
|
const result = sumQuantities([
|
|
{value: '1', unit: 'lb'},
|
|
{value: '1', unit: 'gallon'},
|
|
]);
|
|
expect(result.totals).toHaveLength(2);
|
|
const unitNames = result.totals.map(t => t.unit).sort();
|
|
expect(unitNames).toEqual(['gallon', 'lb']);
|
|
});
|
|
|
|
test('counts unitless / non-standard-unit entries separately', () => {
|
|
const result = sumQuantities([
|
|
{value: '5', unit: 'each'},
|
|
{value: '3', unit: ''},
|
|
{value: '1', unit: 'lb'},
|
|
]);
|
|
expect(result.unitlessCount).toBeCloseTo(8, 5);
|
|
expect(result.totals).toHaveLength(1);
|
|
expect(result.totals[0].unit).toBe('lb');
|
|
});
|
|
});
|
|
|
|
describe('formatTotals', () => {
|
|
test('joins totals and unitless count', () => {
|
|
const summed = {totals: [{magnitude: 1.5, unit: 'lb'}, {magnitude: 2, unit: 'gallon'}], unitlessCount: 3};
|
|
expect(formatTotals(summed)).toBe('1.5 lb + 2 gallon + 3 unitless');
|
|
});
|
|
|
|
test('em-dash when empty', () => {
|
|
expect(formatTotals({totals: [], unitlessCount: null})).toBe('—');
|
|
});
|
|
});
|