Gegenstands-Verwaltung (Non-Food) neben Lebensmitteln

Die Kategorie bestimmt die Verwaltungsart (food/object). Gegenstaende:
Menge je Lagerort statt Chargen/MHD, Umlagern (ohne Grund) und Entfernen
mit Pflicht-Grund samt Entnahme-Statistik. Beliebig viele eigene Felder
je Kategorie (vererbt an Unterkategorien), "gekauft bei" ueber eine
verwaltbare Shop-Liste und ein Produktlink. Barcode-Lookup zusaetzlich
ueber Open Products Facts.

Der Lebensmittel-Teil (Chargen/MHD/FEFO) bleibt unveraendert. Umgesetzt in
Backend (FastAPI, +Tests gruen), Web (React) und iOS (SwiftUI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-25 15:33:56 +02:00
parent 580afcc133
commit 5b952524d7
29 changed files with 3116 additions and 115 deletions

99
web/src/fields.jsx Normal file
View File

@@ -0,0 +1,99 @@
// Selbst definierte Felder: Typen-Registry und die passenden Eingabe-Elemente.
// Bewusst schlank gehalten und aus einer Konfiguration heraus gerendert analog
// zur Karten-Registry des Dashboards (dashboard/cards.jsx).
// Auswahl im Feld-Verwaltungs-Dialog (Reihenfolge = Anzeige).
export const FIELD_TYPES = [
{ value: "text", label: "Text (einzeilig)" },
{ value: "textarea", label: "Text (mehrzeilig)" },
{ value: "number", label: "Zahl (mit Einheit)" },
{ value: "date", label: "Datum" },
{ value: "select", label: "Auswahlliste" },
{ value: "boolean", label: "Ja/Nein" },
];
export function fieldTypeLabel(value) {
return FIELD_TYPES.find((t) => t.value === value)?.label || value;
}
/** Eine einzelne Eingabe passend zum Feldtyp. */
export function FieldInput({ field, value, onChange, disabled }) {
const v = value ?? "";
switch (field.field_type) {
case "textarea":
return (
<textarea rows={3} value={v} disabled={disabled}
onChange={(e) => onChange(e.target.value)} />
);
case "number":
return (
<span className="field-inline">
<input type="number" step="any" value={v} disabled={disabled}
onChange={(e) => onChange(e.target.value)} />
{field.unit && <span className="muted">{field.unit}</span>}
</span>
);
case "date":
return (
<input type="date" value={v} disabled={disabled}
onChange={(e) => onChange(e.target.value)} />
);
case "select":
return (
<select value={v} disabled={disabled}
onChange={(e) => onChange(e.target.value)}>
<option value=""> keine </option>
{(field.options || []).map((o) => (
<option key={o} value={o}>{o}</option>
))}
</select>
);
case "boolean":
return (
<label className="check-inline">
<input type="checkbox" checked={v === "true"} disabled={disabled}
onChange={(e) => onChange(e.target.checked ? "true" : "false")} />
<span className="muted">Ja</span>
</label>
);
default:
return (
<input value={v} disabled={disabled}
onChange={(e) => onChange(e.target.value)} />
);
}
}
/**
* Rendert die effektiven (vererbten) Felder einer Kategorie als Formularblock.
*
* ``values`` ist eine Map { String(feld_id): Wert }, wie sie das Backend liefert
* und erwartet. ``onChange(feldId, wert)`` meldet Änderungen zurück.
*/
export function DynamicFields({ fields, values, onChange, disabled }) {
if (!fields || fields.length === 0) return null;
return (
<div className="stack">
{fields.map((f) => (
<label key={f.id}>
<span className="feldkopf">
{f.label}
{f.field_type === "number" && f.unit ? ` (${f.unit})` : ""}
{f.required ? " *" : ""}
{f.inherited && (
<span className="badge nowrap" title={"geerbt von einer Oberkategorie"}>
geerbt
</span>
)}
</span>
<FieldInput
field={f}
value={values[String(f.id)]}
onChange={(val) => onChange(String(f.id), val)}
disabled={disabled}
/>
</label>
))}
</div>
);
}