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:
37
web/src/pages/ItemResolve.jsx
Normal file
37
web/src/pages/ItemResolve.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { api } from "../api";
|
||||
import Icon from "../components/Icon";
|
||||
|
||||
/**
|
||||
* Ziel eines gescannten QR-Codes (/i/<uid>): löst die UID auf und öffnet den
|
||||
* zugehörigen Artikel mit dem Einzelstück im Blick.
|
||||
*/
|
||||
export default function ItemResolve() {
|
||||
const { uid } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let ok = true;
|
||||
api.itemByUid(uid)
|
||||
.then((item) => {
|
||||
if (ok) navigate(`/products/${item.product_id}?item=${encodeURIComponent(item.uid)}`,
|
||||
{ replace: true });
|
||||
})
|
||||
.catch((err) => { if (ok) setError(err.message); });
|
||||
return () => { ok = false; };
|
||||
}, [uid, navigate]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>Einzelstück {uid}</h1>
|
||||
<div className="sub">{error ? error : "Wird geöffnet…"}</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user