wrolpi/app/src/components/inventory/FieldCell.js
Roland 3d6aead072
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Controller Tests / controller-tests (push) Has been cancelled
Jest Tests / jest-tests (push) Has been cancelled
Rebuild Inventory as a config-only system with food catalog and calculators
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>
2026-06-14 17:14:54 -06:00

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};