mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Build on the rebuilt config-only Inventory with three related additions.
Recommended example inventory:
- A fresh install now seeds the "Food Storage" example with the recommended
one-adult / one-year (~3,000 kcal/day) emergency food supply (60 items,
~1.12M kcal), instead of an empty inventory.
- The counts are layered on top of the shipped food catalog (single source of
per-package nutrition) via RECOMMENDED_ONE_YEAR_FOOD_COUNTS, joined at seed
time by recommended_food_storage_items().
- Genericized the two remaining brand names in the catalog defaults
(Velveeta -> Processed Cheese, Spam -> Canned Lunch Meat) so both the catalog
and the seeded inventory stay brand-free.
Ration tab (split out of Summary):
- Moved the Ration Estimate (People / Daily Calories / estimate table) out of
the increasingly long Summary tab into its own "Ration" tab, shown only for
inventories that have a calories field.
Supply planner ("Plan More Supply"):
- A slider picks a target duration longer than the current estimate; the whole
inventory is scaled up proportionally (every item by the same factor, mix
kept balanced) into a sortable shopping list of what to buy.
- Any split package rounds UP to a whole one (ceil), matching the "1.6 buckets
-> buy 1 more" rule.
- The shopping list is exportable as CSV (reusing toCSV) and PDF (reusing the
print-to-PDF path, scoped via body.printing-shopping so only the list prints
-- no inventory or summary).
Search filter:
- Moved the items search out of the top toolbar (which implied it filtered
every tab) into the Items tab, so it clearly narrows Items only;
Summary/Ration/Export/print always use the full inventory.
Aggregation/role helpers (findNameKey, planSupplyPurchase) live in summarize.js,
the single source of truth shared by Summary, export, and the ration views.
All new pure functions and UI flows are covered by tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
2.8 KiB
JavaScript
63 lines
2.8 KiB
JavaScript
import {formatValue} from "./InventoryTable";
|
|
|
|
// Fields in display (column) order.
|
|
function orderedFields(fields) {
|
|
return [...(fields || [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
}
|
|
|
|
// Quote a single CSV cell per RFC 4180: wrap in double-quotes (escaping internal quotes by doubling) when the value
|
|
// contains a comma, quote, or newline.
|
|
function escapeCell(value) {
|
|
const s = value == null ? '' : String(value);
|
|
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
}
|
|
|
|
/**
|
|
* Build an RFC-4180 CSV string for an inventory: a header row of field labels followed by one row per item, using
|
|
* the same value formatting as the on-screen table (quantity fields render as `<magnitude> <unit>`).
|
|
*/
|
|
export function toCSV(fields, items) {
|
|
const cols = orderedFields(fields);
|
|
const header = cols.map(f => escapeCell(f.label || f.key));
|
|
const rows = (items || []).map(item => cols.map(f => escapeCell(formatValue(item, f))));
|
|
return [header, ...rows].map(row => row.join(',')).join('\r\n');
|
|
}
|
|
|
|
// Column layout for an exported supply-plan shopping list, mirroring the on-screen table. No `type` is set, so
|
|
// the shared CSV writer formats every value as plain text/number.
|
|
const SHOPPING_LIST_FIELDS = [
|
|
{key: 'name', label: 'Item'},
|
|
{key: 'current', label: 'Have'},
|
|
{key: 'additional', label: 'Buy'},
|
|
{key: 'target', label: 'New Total'},
|
|
];
|
|
|
|
// Build a CSV of a supply-plan shopping list (rows from planSupplyPurchase: {name, current, additional, target}),
|
|
// reusing the inventory CSV writer so quoting/escaping stays identical.
|
|
export function shoppingListCSV(rows) {
|
|
return toCSV(SHOPPING_LIST_FIELDS, rows || []);
|
|
}
|
|
|
|
// Filesystem-safe export filename derived from the inventory name (e.g. "Food Storage" -> "food-storage.csv").
|
|
export function inventoryExportFilename(name, ext) {
|
|
const base = (name || 'inventory').trim().toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'inventory';
|
|
return `${base}.${ext}`;
|
|
}
|
|
|
|
// Trigger a browser download of `csv` as `filename`. A UTF-8 BOM is prepended so spreadsheet apps (notably Excel)
|
|
// detect the encoding and render non-ASCII characters correctly.
|
|
export function downloadCSV(filename, csv) {
|
|
const BOM = String.fromCharCode(0xFEFF);
|
|
const blob = new Blob([BOM + csv], {type: 'text/csv;charset=utf-8'});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
// The anchor must be attached to the document for click() to trigger a download in some browsers, and the
|
|
// object URL must stay alive until the browser has started fetching it — so revoke on the next tick.
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
}
|