wrolpi/app/src/components/inventory/InventoryTable.js

332 lines
14 KiB
JavaScript
Raw Normal View History

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 14:20:10 -06:00
import React, {useMemo, useRef, useState} from "react";
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
import {Button, Checkbox, Icon, Table} from "../ui";
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 14:20:10 -06:00
import {FieldCell} from "./FieldCell";
Add "Count by Weight" to estimate item counts from weighing Counting bulk hardware (screws, nails) by hand is slow. This adds a workflow to estimate the count from weight instead: weigh one item, weigh the lot, and the count is filled in automatically. - The Fields modal gains a "Count by Weight" button that adds two quantity fields, Unit Weight and Total Weight (default grams), and tags the Count field as computed. An existing `count` field is linked in place rather than duplicated, so hand-counted items keep their values. - The link is plain metadata on the Count field (`compute: {kind: 'count_by_weight', total, unit}`), so it round-trips through the config/API with no backend change (covered by a backend test). - During item entry, Count = round(Total Weight / Unit Weight), converting mismatched units first (g/kg/oz) and refusing to divide when only one side has a unit. It rounds to a whole number (no 403.7 nails). - When a weight changes, the count always reflects the new reality: it is set to the computed value (0 for a sub-unit total) when both weights are present, or cleared when they can't produce one (a weight blanked, incompatible units) — so a stale count can never persist against edited weights. A hand-typed count is only touched when a weight actually changes. countByWeight lives in units.js; the field preset, config detection, and the entry-time apply hook live in a new computeFields.js. Covered by unit tests (countByWeight, computeFields), InventoryTable auto-fill / clear / sub-unit on draft and inline edit, the FieldSchemaEditor preset, and a backend field-metadata round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:36:26 -06:00
import {applyComputedCounts, computedCountConfigs} from "./computeFields";
import {NUMERIC_TYPES} from "./units";
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 14:20:10 -06:00
const LOCATION_LIST_ID = 'inventory-location-suggestions';
const CATALOG_LIST_ID = 'inventory-catalog-names';
// Fields copied from a catalog entry onto the new-item row when its name matches a catalog entry.
const CATALOG_KEYS = ['name', 'category', 'subcategory', 'item_size', 'item_size_unit', 'calories'];
// Build an empty item keyed by the field schema (quantity fields seed their default unit).
function emptyItem(fields) {
const item = {};
fields.forEach(f => {
item[f.key] = '';
if (f.type === 'quantity') {
item[`${f.key}_unit`] = f.unit || '';
}
});
return item;
}
function nextId(items) {
return items.reduce((max, i) => (typeof i.id === 'number' && i.id > max ? i.id : max), 0) + 1;
}
// Type-aware comparison for sorting: numeric for number/quantity/calories, chronological for date, locale string
// otherwise. Blank values sort last (ascending).
function compareByField(a, b, field) {
const av = a[field.key], bv = b[field.key];
const aBlank = av === '' || av == null;
const bBlank = bv === '' || bv == null;
if (aBlank || bBlank) {
return aBlank === bBlank ? 0 : (aBlank ? 1 : -1);
}
if (NUMERIC_TYPES.includes(field.type)) {
return (Number(av) || 0) - (Number(bv) || 0);
}
if (field.type === 'date') {
return (Date.parse(av) || 0) - (Date.parse(bv) || 0);
}
return String(av).localeCompare(String(bv));
}
/**
* Spreadsheet-style item table. All edits happen on the in-memory items; `onChange(newItems)` persists the whole
* inventory (the page holds every inventory and saves the changed one). A persistent "new item" row sits at the
* bottom: Tab moves between fields and Enter submits the item then refocuses the first field.
*/
Add CSV/PDF export and a search filter to Inventory New Export tab with two client-side formats (no new dependencies): - CSV: the raw items table (RFC-4180 quoting, UTF-8 BOM for Excel), generated entirely in the browser. The download anchor is attached to the DOM and the object URL is revoked on the next tick for cross-browser robustness. - PDF: a printable full-inventory table followed by a group-by summary table, via the browser's print dialog ("Save as PDF"). The summary's grouping is chosen in the Export tab. Search filter: a box on its own line under the inventory selector filters the active inventory across every column's displayed value (case-insensitive; whitespace-separated terms are AND-ed). It narrows the whole view — Items table, Summary aggregation, and Export/PDF — while edits/adds in the table still operate on the full inventory, so filtering never drops data. Summary/export grouping: - The group-by aggregation is extracted into a shared summarize.js (field-role detection + count-aware totals via units.js + calories), now the single source of truth for the Summary tab, the PDF summary, and the ration calculator. findCaloriesKey/findCountKey moved here from RationCalculator (re-exported for back-compat). - The Sum selector now offers number and calories fields in addition to quantity: number/calories are summed as a plain column total, while quantity stays count-aware and unit-compacted and the dedicated Calories column stays count-aware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:00:01 -06:00
export function InventoryTable({slug, fields, items, locations, catalog, search, onChange}) {
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 14:20:10 -06:00
const [draft, setDraft] = useState(() => emptyItem(fields));
const [edits, setEdits] = useState({}); // itemId -> edited copy
const [selected, setSelected] = useState(new Set());
const [sort, setSort] = useState(null); // {key, dir: 'asc'|'desc'} or null for entry order
const [focusCell, setFocusCell] = useState(null); // {id, key}: the cell to focus + select on entering edit
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 14:20:10 -06:00
const firstInputRef = useRef(null);
const hasLocationField = fields.some(f => f.type === 'location');
const hasNameField = fields.some(f => f.key === 'name');
// Catalog lookup by lower-cased name, and the set of field keys this inventory actually has (so pre-fill only
// touches existing fields). Catalog integration only applies when the inventory has a `name` field.
const catalogByName = useMemo(() => {
const map = {};
(catalog || []).forEach(e => {
if (e.name) {
map[e.name.trim().toLowerCase()] = e;
}
});
return map;
}, [catalog]);
const fieldKeySet = useMemo(() => {
const keys = new Set();
fields.forEach(f => {
keys.add(f.key);
if (f.type === 'quantity') {
keys.add(`${f.key}_unit`);
}
});
return keys;
}, [fields]);
Add "Count by Weight" to estimate item counts from weighing Counting bulk hardware (screws, nails) by hand is slow. This adds a workflow to estimate the count from weight instead: weigh one item, weigh the lot, and the count is filled in automatically. - The Fields modal gains a "Count by Weight" button that adds two quantity fields, Unit Weight and Total Weight (default grams), and tags the Count field as computed. An existing `count` field is linked in place rather than duplicated, so hand-counted items keep their values. - The link is plain metadata on the Count field (`compute: {kind: 'count_by_weight', total, unit}`), so it round-trips through the config/API with no backend change (covered by a backend test). - During item entry, Count = round(Total Weight / Unit Weight), converting mismatched units first (g/kg/oz) and refusing to divide when only one side has a unit. It rounds to a whole number (no 403.7 nails). - When a weight changes, the count always reflects the new reality: it is set to the computed value (0 for a sub-unit total) when both weights are present, or cleared when they can't produce one (a weight blanked, incompatible units) — so a stale count can never persist against edited weights. A hand-typed count is only touched when a weight actually changes. countByWeight lives in units.js; the field preset, config detection, and the entry-time apply hook live in a new computeFields.js. Covered by unit tests (countByWeight, computeFields), InventoryTable auto-fill / clear / sub-unit on draft and inline edit, the FieldSchemaEditor preset, and a backend field-metadata round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:36:26 -06:00
// "Count by weight" configs: a Count field auto-filled from a Total Weight ÷ Unit Weight.
const countConfigs = useMemo(() => computedCountConfigs(fields), [fields]);
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 14:20:10 -06:00
const listIdFor = (field) => {
if (field.type === 'location') {
return LOCATION_LIST_ID;
}
if (field.key === 'name' && (catalog || []).length) {
return CATALOG_LIST_ID;
}
return undefined;
};
// Re-seed local editing state when the inventory or its schema changes.
React.useEffect(() => {
setDraft(emptyItem(fields));
setEdits({});
setSelected(new Set());
setSort(null);
setFocusCell(null);
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 14:20:10 -06:00
}, [slug, fields]);
// Expired = any date-type field strictly before the start of today (shared with the mobile view).
const isExpired = (item) => isItemExpired(item, fields);
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 14:20:10 -06:00
const toggleSort = (key) => setSort(prev =>
prev && prev.key === key
? {key, dir: prev.dir === 'asc' ? 'desc' : 'asc'}
: {key, dir: 'asc'});
// Rows are sorted for display only; the persistent entry row lives in the footer and is unaffected.
const displayItems = useMemo(() => {
const list = items || [];
if (!sort) {
return list;
}
const field = fields.find(f => f.key === sort.key);
if (!field) {
return list;
}
const sorted = [...list].sort((a, b) => compareByField(a, b, field));
return sort.dir === 'desc' ? sorted.reverse() : sorted;
}, [items, fields, sort]);
Add CSV/PDF export and a search filter to Inventory New Export tab with two client-side formats (no new dependencies): - CSV: the raw items table (RFC-4180 quoting, UTF-8 BOM for Excel), generated entirely in the browser. The download anchor is attached to the DOM and the object URL is revoked on the next tick for cross-browser robustness. - PDF: a printable full-inventory table followed by a group-by summary table, via the browser's print dialog ("Save as PDF"). The summary's grouping is chosen in the Export tab. Search filter: a box on its own line under the inventory selector filters the active inventory across every column's displayed value (case-insensitive; whitespace-separated terms are AND-ed). It narrows the whole view — Items table, Summary aggregation, and Export/PDF — while edits/adds in the table still operate on the full inventory, so filtering never drops data. Summary/export grouping: - The group-by aggregation is extracted into a shared summarize.js (field-role detection + count-aware totals via units.js + calories), now the single source of truth for the Summary tab, the PDF summary, and the ration calculator. findCaloriesKey/findCountKey moved here from RationCalculator (re-exported for back-compat). - The Sum selector now offers number and calories fields in addition to quantity: number/calories are summed as a plain column total, while quantity stays count-aware and unit-compacted and the dedicated Calories column stays count-aware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:00:01 -06:00
// The search filters which rows are shown; mutations below still operate on the full `items` prop.
const visibleItems = useMemo(() => filterItems(displayItems, fields, search), [displayItems, fields, search]);
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 14:20:10 -06:00
const focusFirst = () => {
ui: the last of Semantic UI, in name and in dead CSS The library has been uninstalled for some time, but the codebase still read as though it were there: identifiers named after it, rules written against its class names, and about 180 comments explaining a rule in terms of a thing that is no longer installed. Renamed, keeping the vocabulary the app actually uses: semanticUIColorMap -> navColorHexMap, semanticColorNames -> paletteColorNames, semanticNames -> iconNameAliases, semanticSizes -> sizeAliases, and SemanticUIColors -> NavColors in wrolpi/schema.py, which is what validates nav_color on the way in. ColorToSemanticHexColor had no callers at all and is gone. The baseline ratchet has done its job -- the list reached zero, so the JSON is deleted and the guard keeps only the checks with teeth: no package, no import, no `className="ui "`, no toast library. Its Theme.tsx check went with the wrappers it was watching for. It also asserts its own walk found files, so a clean result cannot mean it scanned nothing. Deleted from App.css: every rule requiring `.ui`, `.inverted`, `.segment` or `.modal`, none of which any component has set since the stylesheet stopped loading, plus the form skeletons, ui-alerts and progress-text rules that nothing renders. Two of them were doing harm rather than nothing: the drag tints had a dark-theme variant under `.inverted` that stopped applying, leaving 15% of a fixed blue over a dark row, and now mix a token instead. Two selectors naming markup that no longer exists were live bugs. The file browser's filter had `.ui.input.file-browser-filter`, so it rendered at the browser default width on a bar laid out to give it 34em -- now qualified by its container, because `.wrolpi-action-input` sets `width: 100%` at the same specificity and is loaded later. And SearchBox takes named props rather than a rest spread, so the `id` DestinationForm hands it was dropped in silence and the label it renders pointed at nothing; the download forms' Destination field has had no associated label since the migration. "Semantic roles" survives in four places, where it means what it says. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 19:15:57 -06:00
// Our inputs forward their ref straight to the underlying <input> DOM node (unlike the old Input,
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
// which wrapped it behind `.inputRef`).
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 14:20:10 -06:00
const node = firstInputRef.current;
if (node && node.focus) {
node.focus();
}
};
const setDraftValue = (key, value) => setDraft(prev => {
const next = {...prev, [key]: value};
// When the Name matches a catalog entry, pre-fill the matching fields (category, size, calories, ...).
if (key === 'name' && hasNameField) {
const entry = catalogByName[String(value).trim().toLowerCase()];
if (entry) {
CATALOG_KEYS.forEach(k => {
if (k !== 'name' && fieldKeySet.has(k) && entry[k] != null && entry[k] !== '') {
next[k] = entry[k];
}
});
}
}
Add "Count by Weight" to estimate item counts from weighing Counting bulk hardware (screws, nails) by hand is slow. This adds a workflow to estimate the count from weight instead: weigh one item, weigh the lot, and the count is filled in automatically. - The Fields modal gains a "Count by Weight" button that adds two quantity fields, Unit Weight and Total Weight (default grams), and tags the Count field as computed. An existing `count` field is linked in place rather than duplicated, so hand-counted items keep their values. - The link is plain metadata on the Count field (`compute: {kind: 'count_by_weight', total, unit}`), so it round-trips through the config/API with no backend change (covered by a backend test). - During item entry, Count = round(Total Weight / Unit Weight), converting mismatched units first (g/kg/oz) and refusing to divide when only one side has a unit. It rounds to a whole number (no 403.7 nails). - When a weight changes, the count always reflects the new reality: it is set to the computed value (0 for a sub-unit total) when both weights are present, or cleared when they can't produce one (a weight blanked, incompatible units) — so a stale count can never persist against edited weights. A hand-typed count is only touched when a weight actually changes. countByWeight lives in units.js; the field preset, config detection, and the entry-time apply hook live in a new computeFields.js. Covered by unit tests (countByWeight, computeFields), InventoryTable auto-fill / clear / sub-unit on draft and inline edit, the FieldSchemaEditor preset, and a backend field-metadata round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:36:26 -06:00
// Auto-fill any "count by weight" field when a weight (or its unit) changed.
applyComputedCounts(next, countConfigs, key);
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 14:20:10 -06:00
return next;
});
const submitDraft = () => {
const hasValue = Object.entries(draft).some(([k, v]) => !k.endsWith('_unit') && v !== '' && v != null);
if (!hasValue) {
return;
}
onChange([...items, {...draft, id: nextId(items)}]);
setDraft(emptyItem(fields));
focusFirst();
};
const startEdit = (item, key) => {
setEdits(prev => ({...prev, [item.id]: {...item}}));
setFocusCell({id: item.id, key}); // the FieldCell for this key auto-focuses and selects its contents
};
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 14:20:10 -06:00
const setEditValue = (id, key, value) =>
Add "Count by Weight" to estimate item counts from weighing Counting bulk hardware (screws, nails) by hand is slow. This adds a workflow to estimate the count from weight instead: weigh one item, weigh the lot, and the count is filled in automatically. - The Fields modal gains a "Count by Weight" button that adds two quantity fields, Unit Weight and Total Weight (default grams), and tags the Count field as computed. An existing `count` field is linked in place rather than duplicated, so hand-counted items keep their values. - The link is plain metadata on the Count field (`compute: {kind: 'count_by_weight', total, unit}`), so it round-trips through the config/API with no backend change (covered by a backend test). - During item entry, Count = round(Total Weight / Unit Weight), converting mismatched units first (g/kg/oz) and refusing to divide when only one side has a unit. It rounds to a whole number (no 403.7 nails). - When a weight changes, the count always reflects the new reality: it is set to the computed value (0 for a sub-unit total) when both weights are present, or cleared when they can't produce one (a weight blanked, incompatible units) — so a stale count can never persist against edited weights. A hand-typed count is only touched when a weight actually changes. countByWeight lives in units.js; the field preset, config detection, and the entry-time apply hook live in a new computeFields.js. Covered by unit tests (countByWeight, computeFields), InventoryTable auto-fill / clear / sub-unit on draft and inline edit, the FieldSchemaEditor preset, and a backend field-metadata round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:36:26 -06:00
setEdits(prev => {
const updated = {...prev[id], [key]: value};
applyComputedCounts(updated, countConfigs, key);
return {...prev, [id]: updated};
});
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 14:20:10 -06:00
const saveEdit = (id) => {
onChange(items.map(i => i.id === id ? edits[id] : i));
setEdits(prev => {
const next = {...prev};
delete next[id];
return next;
});
};
const toggleSelected = (id) => setSelected(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
const removeSelected = () => {
if (selected.size === 0) {
return;
}
onChange(items.filter(i => !selected.has(i.id)));
setSelected(new Set());
};
const sortedFields = [...fields].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return <>
{/* Shared location suggestions (pooled across all inventories), referenced by location-field inputs. */}
{hasLocationField &&
<datalist id={LOCATION_LIST_ID}>
{(locations || []).map(l => <option key={l} value={l}/>)}
</datalist>}
{/* Food-catalog name suggestions; selecting one pre-fills the matching fields. */}
{hasNameField && (catalog || []).length > 0 &&
<datalist id={CATALOG_LIST_ID}>
{catalog.map(e => <option key={e.id ?? e.name} value={e.name}/>)}
</datalist>}
{selected.size > 0 &&
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
<Button role='danger' onClick={removeSelected} style={{marginBottom: '0.5em'}}>
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 14:20:10 -06:00
Delete {selected.size} selected
</Button>}
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell/>
{sortedFields.map(f => <Table.HeaderCell
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 14:20:10 -06:00
key={f.key}
sorted={sort && sort.key === f.key ? (sort.dir === 'asc' ? 'ascending' : 'descending') : undefined}
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
onSort={() => toggleSort(f.key)}>
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 14:20:10 -06:00
{f.label}
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
</Table.HeaderCell>)}
</Table.Row>
</Table.Header>
<Table.Body>
Add CSV/PDF export and a search filter to Inventory New Export tab with two client-side formats (no new dependencies): - CSV: the raw items table (RFC-4180 quoting, UTF-8 BOM for Excel), generated entirely in the browser. The download anchor is attached to the DOM and the object URL is revoked on the next tick for cross-browser robustness. - PDF: a printable full-inventory table followed by a group-by summary table, via the browser's print dialog ("Save as PDF"). The summary's grouping is chosen in the Export tab. Search filter: a box on its own line under the inventory selector filters the active inventory across every column's displayed value (case-insensitive; whitespace-separated terms are AND-ed). It narrows the whole view — Items table, Summary aggregation, and Export/PDF — while edits/adds in the table still operate on the full inventory, so filtering never drops data. Summary/export grouping: - The group-by aggregation is extracted into a shared summarize.js (field-role detection + count-aware totals via units.js + calories), now the single source of truth for the Summary tab, the PDF summary, and the ration calculator. findCaloriesKey/findCountKey moved here from RationCalculator (re-exported for back-compat). - The Sum selector now offers number and calories fields in addition to quantity: number/calories are summed as a plain column total, while quantity stays count-aware and unit-compacted and the dedicated Calories column stays count-aware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:00:01 -06:00
{visibleItems.map(item => {
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 14:20:10 -06:00
const editing = edits[item.id];
const expired = isExpired(item);
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
return <Table.Row key={item.id} failed={expired}>
<Table.Cell>
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 14:20:10 -06:00
<Checkbox checked={selected.has(item.id)} onChange={() => toggleSelected(item.id)}/>
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
{expired && <Icon name='warning sign' label='Expired'
ui: route genuine success and failure through the roles Sixteen call sites, chosen by reading each one rather than by grepping for a hue. Four sub-agents judged them independently against one rule: convert only what genuinely means success or failure; identification, category and decoration stay hue-named. Converted: SMART drive health (all four branches -- PASS/WARN/FAIL/unknown) config validity: valid / invalid / empty "Failed to load preview" the two "Download error" triggers expired inventory items, desktop and mobile the refresh phases: complete / active / not reached a refresh Progress bar in its error state cookie status: Unlocked / Locked a chip-mismatch label on the flasher CPU load thresholds (LoadStatistic) the outcome of an API call in useAPIButton the gallery's "Danger zone" heading and its load readings Left alone, deliberately: the file browser toolbar -- every action has its own colour to be told apart, and delete already uses role='danger' (Roland's ruling) diff add/remove in the config and inventory import previews. Adding is not a success and removing is not a failure; in Overwrite mode the removals are exactly what the user asked for, and painting them danger would report their intended action as broken seasons in DatesSelector, the uploader star and favourite heart, TagIcon, downloader category buttons, "Recommended to Keep" Three sites were wrong in a way worth naming. Both "Download error" triggers guard on the same condition and render the same modal, but one was `orange` and the other `red` -- and `orange` is the single worst choice, byte-identical to `--text` in night, so that error button was indistinguishable from an ordinary control. Config validity drew its valid state in `--violet`, which no user parses as "valid". And `LoadStatistic` -- the thing users actually watch on the Status page -- rendered its warning threshold in that same invisible orange. Also fixed a stale comment in Videos.js that claimed no token existed for these. Two notes on the tests: The first LoadStatistic test I wrote rendered the component and asserted the inline colour. jsdom REJECTS `color: var(--danger)` as an invalid declaration and drops it, so every colour reads back as the empty string -- two of the four assertions passed for that reason alone and would have kept passing with the hues restored. It is a source guard now, with the painted result asserted in the browser instead. That browser test then found something real: an uncoloured reading inherits `--text`, which in night is 4.98:1 against `--warning`'s 4.91:1, so a machine under load is marginally QUIETER than one that is fine. Not fixed here. The only way up on a dark panel is lightness, and night's danger would desaturate from #ff3e3e to about #ff5d5d -- trading the theme's "red only" character for loudness. That is a palette decision, so the test documents it and asserts what does hold rather than quietly making it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 18:08:34 -06:00
style={{color: 'var(--danger)', marginLeft: '0.4em'}}/>}
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
</Table.Cell>
{sortedFields.map(f => <Table.Cell key={f.key}>
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 14:20:10 -06:00
{editing
? <FieldCell
field={f}
value={editing[f.key]}
unitValue={editing[`${f.key}_unit`]}
onChange={v => setEditValue(item.id, f.key, v)}
onUnitChange={v => setEditValue(item.id, `${f.key}_unit`, v)}
onEnter={() => saveEdit(item.id)}
listId={listIdFor(f)}
autoFocus={focusCell && focusCell.id === item.id && focusCell.key === f.key}
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 14:20:10 -06:00
/>
: <span onClick={() => startEdit(item, f.key)} style={{cursor: 'pointer'}}>
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 14:20:10 -06:00
{formatValue(item, f)}
</span>}
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
</Table.Cell>)}
</Table.Row>;
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 14:20:10 -06:00
})}
Add CSV/PDF export and a search filter to Inventory New Export tab with two client-side formats (no new dependencies): - CSV: the raw items table (RFC-4180 quoting, UTF-8 BOM for Excel), generated entirely in the browser. The download anchor is attached to the DOM and the object URL is revoked on the next tick for cross-browser robustness. - PDF: a printable full-inventory table followed by a group-by summary table, via the browser's print dialog ("Save as PDF"). The summary's grouping is chosen in the Export tab. Search filter: a box on its own line under the inventory selector filters the active inventory across every column's displayed value (case-insensitive; whitespace-separated terms are AND-ed). It narrows the whole view — Items table, Summary aggregation, and Export/PDF — while edits/adds in the table still operate on the full inventory, so filtering never drops data. Summary/export grouping: - The group-by aggregation is extracted into a shared summarize.js (field-role detection + count-aware totals via units.js + calories), now the single source of truth for the Summary tab, the PDF summary, and the ration calculator. findCaloriesKey/findCountKey moved here from RationCalculator (re-exported for back-compat). - The Sum selector now offers number and calories fields in addition to quantity: number/calories are summed as a plain column total, while quantity stays count-aware and unit-compacted and the dedicated Calories column stays count-aware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:00:01 -06:00
{search && visibleItems.length === 0 && (items || []).length > 0 &&
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
<Table.Row>
<Table.Cell colSpan={sortedFields.length + 1} style={{opacity: 0.7}}>
Add CSV/PDF export and a search filter to Inventory New Export tab with two client-side formats (no new dependencies): - CSV: the raw items table (RFC-4180 quoting, UTF-8 BOM for Excel), generated entirely in the browser. The download anchor is attached to the DOM and the object URL is revoked on the next tick for cross-browser robustness. - PDF: a printable full-inventory table followed by a group-by summary table, via the browser's print dialog ("Save as PDF"). The summary's grouping is chosen in the Export tab. Search filter: a box on its own line under the inventory selector filters the active inventory across every column's displayed value (case-insensitive; whitespace-separated terms are AND-ed). It narrows the whole view — Items table, Summary aggregation, and Export/PDF — while edits/adds in the table still operate on the full inventory, so filtering never drops data. Summary/export grouping: - The group-by aggregation is extracted into a shared summarize.js (field-role detection + count-aware totals via units.js + calories), now the single source of truth for the Summary tab, the PDF summary, and the ration calculator. findCaloriesKey/findCountKey moved here from RationCalculator (re-exported for back-compat). - The Sum selector now offers number and calories fields in addition to quantity: number/calories are summed as a plain column total, while quantity stays count-aware and unit-compacted and the dedicated Calories column stays count-aware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:00:01 -06:00
No items match "{search}".
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
</Table.Cell>
</Table.Row>}
</Table.Body>
<Table.Footer>
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 14:20:10 -06:00
{/* The persistent new-item entry row. */}
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
<Table.Row>
<Table.Cell>
<Button role='primary' size='xs' onClick={submitDraft} aria-label='Add item'>+</Button>
</Table.Cell>
{sortedFields.map((f, index) => <Table.Cell key={f.key}>
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 14:20:10 -06:00
<FieldCell
field={f}
value={draft[f.key]}
unitValue={draft[`${f.key}_unit`]}
onChange={v => setDraftValue(f.key, v)}
onUnitChange={v => setDraftValue(`${f.key}_unit`, v)}
onEnter={submitDraft}
inputRef={index === 0 ? firstInputRef : undefined}
listId={listIdFor(f)}
/>
ui: thirty-five files off Semantic, leaving fifteen Phase 2, part 4. Baseline 50 -> 15. Migrated: all eight calculators, the rest of inventory and collections, the Map/Zim/Docs cluster, Videos, Files, Search, Archive, DashboardPage, DatesSelector, HelpModal, Events, Tags, Flasher's neighbours, admin Settings and Status, Download, and App. Also the three shared modules everything imports: api.js and customHooks.js swap the Semantic toast library for the library's shim (same call shape), and useForm.js renders the library's Message. useForm accepts both the old `{content, header, positive|negative}` shape and the new `{kind, title}`, because the pages building those objects migrate separately. Two library gaps closed, both found by call sites needing them: ActionInput is a forwardRef now. Its ref reaches the input rather than the wrapper, because callers use it to focus or select the field -- a ref to the surrounding div would do neither, which is why one call site had resorted to hand-copying the component's markup. Header takes a `color` token name. Three separate call sites had reached for an inline style to colour a heading that carries meaning ("3 items to add" against "2 to remove"), which is how a hex ends up in markup. Two files needed repair after being interrupted mid-edit: MapViewer had a Segment left in its error branch, and VideoPlayer had lost the useContext import its own code still used. Neither jest nor tsc noticed; only `npm run build` did, which is the whole reason that check exists. Fifteen files remain: Archive, BulkTagModal, Channels, Docs, FileBrowser, FilePreview, Flasher, Nav, Playlists, Theme.tsx, Upload, VideoPlayer, Zim, ControllerPage, admin/Downloads. Theme.tsx is deliberately last -- the unmigrated files still import its Semantic-backed wrappers, so it can only go once they have.
2026-07-30 12:21:30 -06:00
</Table.Cell>)}
</Table.Row>
</Table.Footer>
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 14:20:10 -06:00
</Table>
</>;
}
// Format an item's value for a field for display (shared with the read-only mobile view).
export function formatValue(item, field) {
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 14:20:10 -06:00
const value = item[field.key];
if (value === '' || value == null) {
return '';
}
if (field.type === 'quantity') {
const unit = item[`${field.key}_unit`] || field.unit || '';
return `${value} ${unit}`.trim();
}
return String(value);
}
// True when any `date`-type field is before the start of today (item is expired).
export function isItemExpired(item, fields) {
const start = new Date();
start.setHours(0, 0, 0, 0);
return (fields || []).some(f => {
if (f.type !== 'date') {
return false;
}
const t = Date.parse(item[f.key]);
return !isNaN(t) && t < start.getTime();
});
}
Add CSV/PDF export and a search filter to Inventory New Export tab with two client-side formats (no new dependencies): - CSV: the raw items table (RFC-4180 quoting, UTF-8 BOM for Excel), generated entirely in the browser. The download anchor is attached to the DOM and the object URL is revoked on the next tick for cross-browser robustness. - PDF: a printable full-inventory table followed by a group-by summary table, via the browser's print dialog ("Save as PDF"). The summary's grouping is chosen in the Export tab. Search filter: a box on its own line under the inventory selector filters the active inventory across every column's displayed value (case-insensitive; whitespace-separated terms are AND-ed). It narrows the whole view — Items table, Summary aggregation, and Export/PDF — while edits/adds in the table still operate on the full inventory, so filtering never drops data. Summary/export grouping: - The group-by aggregation is extracted into a shared summarize.js (field-role detection + count-aware totals via units.js + calories), now the single source of truth for the Summary tab, the PDF summary, and the ration calculator. findCaloriesKey/findCountKey moved here from RationCalculator (re-exported for back-compat). - The Sum selector now offers number and calories fields in addition to quantity: number/calories are summed as a plain column total, while quantity stays count-aware and unit-compacted and the dedicated Calories column stays count-aware. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:00:01 -06:00
/**
* Filter items by a free-text search across every column's displayed value (using the same formatting as the
* table, so quantities match as "30 lb", dates as "2030-01-01", etc.). Case-insensitive; whitespace-separated
* terms are AND-ed. A blank search returns all items. Shared by the Items table, Summary, and export views.
*/
export function filterItems(items, fields, search) {
const query = (search || '').trim().toLowerCase();
if (!query) {
return items || [];
}
const terms = query.split(/\s+/);
const cols = [...(fields || [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
return (items || []).filter(item => {
const haystack = cols.map(f => formatValue(item, f)).join(' ').toLowerCase();
return terms.every(term => haystack.includes(term));
});
}