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>
90 lines
3.4 KiB
JavaScript
90 lines
3.4 KiB
JavaScript
import React from "react";
|
|
import {Input, Select} from "semantic-ui-react";
|
|
import {ALL_UNITS, UNIT_GROUPS} from "./units";
|
|
|
|
// Renders the correct editor for a single field/value, wiring up keyboard navigation. The `inputRef` is attached
|
|
// to the primary <input> so the parent table can focus the first cell of a new row.
|
|
//
|
|
// Props:
|
|
// field - {key, label, type, unit?, options?}
|
|
// value - the current item value for field.key
|
|
// unitValue- the current unit (for quantity fields; stored under `${key}_unit`)
|
|
// onChange(value) - value changed
|
|
// onUnitChange(unit) - quantity unit changed
|
|
// onEnter() - Enter pressed (submit row)
|
|
// inputRef - ref attached to the primary input
|
|
export function FieldCell({field, value, unitValue, onChange, onUnitChange, onEnter, inputRef, listId}) {
|
|
const handleKeyDown = (e) => {
|
|
if (e.key === 'Enter' && onEnter) {
|
|
e.preventDefault();
|
|
onEnter();
|
|
}
|
|
};
|
|
|
|
const common = {
|
|
value: value ?? '',
|
|
onChange: (e, data) => onChange(data ? data.value : e.target.value),
|
|
onKeyDown: handleKeyDown,
|
|
'aria-label': field.label,
|
|
name: field.key,
|
|
};
|
|
|
|
if (field.type === 'select') {
|
|
const options = (field.options || []).map(o => ({key: o, value: o, text: o}));
|
|
// Allow clearing.
|
|
return <Select
|
|
fluid search clearable selection
|
|
name={field.key}
|
|
options={options}
|
|
placeholder={field.label}
|
|
value={value ?? ''}
|
|
onChange={(e, data) => onChange(data.value)}
|
|
onKeyDown={handleKeyDown}
|
|
aria-label={field.label}
|
|
/>;
|
|
}
|
|
|
|
if (field.type === 'date') {
|
|
return <Input {...common} type='date' fluid ref={inputRef}/>;
|
|
}
|
|
|
|
if (field.type === 'number' || field.type === 'calories') {
|
|
// `calories` is a number (kcal per unit) that the Summary's ration estimate detects by type.
|
|
return <Input {...common} type='number' fluid ref={inputRef}/>;
|
|
}
|
|
|
|
if (field.type === 'quantity') {
|
|
const unitOptions = ALL_UNITS.map(u => ({key: u, value: u, text: u}));
|
|
return <Input
|
|
type='number'
|
|
fluid
|
|
ref={inputRef}
|
|
name={field.key}
|
|
value={value ?? ''}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
aria-label={field.label}
|
|
label={<Select
|
|
compact
|
|
name={`${field.key}_unit`}
|
|
options={unitOptions}
|
|
value={unitValue ?? field.unit ?? ''}
|
|
onChange={(e, data) => onUnitChange(data.value)}
|
|
aria-label={`${field.label} unit`}
|
|
/>}
|
|
labelPosition='right'
|
|
/>;
|
|
}
|
|
|
|
if (field.type === 'location') {
|
|
// Native datalist autocomplete fed by all inventories' locations (basement, attic, ...). Using a datalist
|
|
// (rather than a Dropdown) keeps it a real text input, so the Tab/Enter row-entry flow still works.
|
|
return <Input {...common} fluid ref={inputRef} list={listId}/>;
|
|
}
|
|
|
|
// text / fallback (supports a datalist via listId, e.g. catalog name suggestions on the Name field).
|
|
return <Input {...common} fluid ref={inputRef} list={listId}/>;
|
|
}
|
|
|
|
// The list of selectable unit groups, re-exported for the field editor.
|
|
export {UNIT_GROUPS};
|