Web: Einzelstücke mit QR – anlegen, je-Stück-Felder, Etiketten, /i/<uid>

Gegenstands-Produkte haben jetzt den Umschalter "Menge je Lagerort / Einzelstücke".
Bei Einzelstücken erscheint (volle Breite) eine Liste je Stueck mit eigener UID,
QR-Code, Lagerort, Kaufdatum, Garantie, Bezugsquelle und Notiz - direkt inline
aenderbar, mit Entfernen (Grund) und druckbaren QR-Etiketten. Ein Scan oeffnet
ueber die Route /i/<uid> das passende Stueck. QR via qrcode (gebundelt, offline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-26 00:36:26 +02:00
parent 5ce4411d1e
commit 6fb5c47de6
7 changed files with 665 additions and 9 deletions

View File

@@ -12,6 +12,7 @@ import { useSettings } from "../settings";
import { asTree } from "../categoryTree";
import { DynamicFields, FIELD_TYPES } from "../fields";
import ObjektBestand from "../components/ObjektBestand";
import Einzelstuecke from "../components/Einzelstuecke";
import {
daysUntil, expiryRowClass, fmt, fromMonthInput, gebinde, isExpired, relativeExpiry,
toMonthInput, unitShort,
@@ -20,7 +21,7 @@ import {
const EMPTY = {
barcode: "", name: "", brand: "", image_url: "",
unit_id: "", package_size: "", package_label: "", date_precision: "day", min_stock: "",
group_id: "", category_id: "", shop_id: "", product_url: "",
group_id: "", category_id: "", shop_id: "", product_url: "", individual: false,
};
// (Gebinde kommen jetzt aus der Verwaltung, siehe api.listPackageTypes)
@@ -384,6 +385,7 @@ export default function ProductForm() {
category_id: p.category_id ?? "",
shop_id: p.shop_id ?? "",
product_url: p.product_url || "",
individual: Boolean(p.individual),
});
setFieldValues(p.field_values || {});
setModus(p.tracking === "object" ? "object" : "food");
@@ -440,10 +442,11 @@ export default function ProductForm() {
Object.entries(fieldValues).forEach(([k, v]) => {
if (erlaubt.has(String(k))) fv[k] = v;
});
return { ...base, field_values: fv };
return { ...base, individual: form.individual, field_values: fv };
}
return {
...base,
individual: false,
unit_id: form.unit_id === "" ? null : Number(form.unit_id),
package_size: form.package_size === "" ? null : Number(form.package_size),
package_label: form.package_label.trim() || null,
@@ -740,13 +743,27 @@ export default function ProductForm() {
{isObject && (<>
<div className="row">
<label className="grow">
Gekauft bei
<select value={form.shop_id} onChange={(e) => set("shop_id", e.target.value)}
disabled={readOnly}>
<option value=""> unbekannt / mehrere </option>
{shops.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
Verwaltung
<div className="segmented">
<button type="button" className={!form.individual ? "active" : ""}
onClick={() => set("individual", false)} disabled={readOnly}>Menge je Lagerort</button>
<button type="button" className={form.individual ? "active" : ""}
onClick={() => set("individual", true)} disabled={readOnly}>Einzelstücke</button>
</div>
</label>
<div className="grow" />
</div>
<div className="row">
{!form.individual && (
<label className="grow">
Gekauft bei
<select value={form.shop_id} onChange={(e) => set("shop_id", e.target.value)}
disabled={readOnly}>
<option value=""> unbekannt / mehrere </option>
{shops.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
</label>
)}
<label className="grow">
Produktlink
<div className="field-inline">
@@ -831,7 +848,7 @@ export default function ProductForm() {
</form>
{!isNew && (isObject ? (
product && (
product && !form.individual && (
<ObjektBestand
product={product}
lots={lots}
@@ -860,6 +877,18 @@ export default function ProductForm() {
))}
</div>
{/* Einzelstücke über die volle Breite die Tabelle hat viele Spalten. */}
{!isNew && product && isObject && form.individual && (
<Einzelstuecke
product={product}
locations={locations}
shops={shops}
isAdmin={isAdmin}
onChanged={async () => { setProduct(await api.getProduct(id)); }}
onError={setError}
/>
)}
{/* Unterhalb der beiden Spalten: die Codes braucht man selten, sie sollen
Formular und Chargen nicht auseinanderschieben. */}
{!isNew && product && (