Amazon-Kaufdatum erkennen; Preis-Vorschlaege beim Anlegen; Lagerort-Dropdowns mit Pfad
1) Beleganalyse erkennt Datumsangaben mit Monatsnamen (DE+EN, z.B. Amazon 'Bestelldatum 29 April 2026'), nicht nur 29.04.2026 - so wird das Kaufdatum solcher Rechnungen gefunden. 2) Beim direkten Anlegen eines Einzelstuecks mit Beleg werden jetzt alle erkannten Preise als Vorschlag angeboten (Dropdown), statt stillschweigend nur den wahrscheinlichsten zu nehmen. 3) Alle Lagerort-Dropdowns zeigen den vollen Pfad (Hedingen -> Keller -> Schublade 1) statt nur den Namen, damit gleichnamige Orte unterscheidbar sind. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,9 +25,33 @@ _PURCHASE_KW = re.compile(
|
|||||||
r"datum|invoice|order date|purchase|receipt",
|
r"datum|invoice|order date|purchase|receipt",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
# 2024-03-31 oder 31.03.2024 / 31/03/24
|
# Monatsnamen (Deutsch + Englisch, mit gängigen Abkürzungen), damit z.B.
|
||||||
|
# Amazon-Rechnungen mit „29 April 2026" erkannt werden – nicht nur 29.04.2026.
|
||||||
|
_MONTHS = {
|
||||||
|
"januar": 1, "jan": 1, "january": 1,
|
||||||
|
"februar": 2, "feb": 2, "february": 2,
|
||||||
|
"märz": 3, "maerz": 3, "mrz": 3, "mär": 3, "mar": 3, "march": 3,
|
||||||
|
"april": 4, "apr": 4,
|
||||||
|
"mai": 5, "may": 5,
|
||||||
|
"juni": 6, "jun": 6, "june": 6,
|
||||||
|
"juli": 7, "jul": 7, "july": 7,
|
||||||
|
"august": 8, "aug": 8,
|
||||||
|
"september": 9, "sept": 9, "sep": 9,
|
||||||
|
"oktober": 10, "okt": 10, "october": 10, "oct": 10,
|
||||||
|
"november": 11, "nov": 11,
|
||||||
|
"dezember": 12, "dez": 12, "december": 12, "dec": 12,
|
||||||
|
}
|
||||||
|
# Längere Namen zuerst, damit „januar" vor „jan" greift.
|
||||||
|
_MONTH_ALT = "|".join(sorted((re.escape(k) for k in _MONTHS), key=len, reverse=True))
|
||||||
|
|
||||||
|
# ISO 2024-03-31, numerisch 31.03.2024 / 31/03/24, oder mit Monatsname
|
||||||
|
# ("29 April 2026" bzw. "April 29, 2026").
|
||||||
_DATE_RE = re.compile(
|
_DATE_RE = re.compile(
|
||||||
r"(\d{4})-(\d{1,2})-(\d{1,2})|(\d{1,2})[.\/](\d{1,2})[.\/](\d{2,4})"
|
r"(?P<iy>\d{4})-(?P<im>\d{1,2})-(?P<idd>\d{1,2})"
|
||||||
|
r"|(?P<d>\d{1,2})[.\/](?P<m>\d{1,2})[.\/](?P<y>\d{2,4})"
|
||||||
|
r"|(?P<nd>\d{1,2})\.?\s+(?P<nmon>" + _MONTH_ALT + r")\.?\s+(?P<ny>\d{4})"
|
||||||
|
r"|(?P<emon>" + _MONTH_ALT + r")\s+(?P<ed>\d{1,2})(?:th|st|nd|rd)?,?\s+(?P<ey>\d{4})",
|
||||||
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
_WINDOW = 60 # Zeichen um ein Stichwort, in denen Zahl/Datum als zugehörig gelten
|
_WINDOW = 60 # Zeichen um ein Stichwort, in denen Zahl/Datum als zugehörig gelten
|
||||||
@@ -45,16 +69,22 @@ def extract_pdf_text(data: bytes) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _match_to_date(m: re.Match) -> date | None:
|
def _match_to_date(m: re.Match) -> date | None:
|
||||||
g = m.groups()
|
g = m.groupdict()
|
||||||
try:
|
try:
|
||||||
if g[0] is not None: # ISO yyyy-mm-dd
|
if g.get("iy"): # ISO yyyy-mm-dd
|
||||||
y, mo, d = int(g[0]), int(g[1]), int(g[2])
|
y, mo, d = int(g["iy"]), int(g["im"]), int(g["idd"])
|
||||||
else: # dd.mm.yyyy / dd/mm/yy
|
elif g.get("d"): # dd.mm.yyyy / dd/mm/yy
|
||||||
d, mo, y = int(g[3]), int(g[4]), int(g[5])
|
d, mo, y = int(g["d"]), int(g["m"]), int(g["y"])
|
||||||
if y < 100:
|
if y < 100:
|
||||||
y += 2000
|
y += 2000
|
||||||
|
elif g.get("nd"): # 29 April 2026
|
||||||
|
d, mo, y = int(g["nd"]), _MONTHS[g["nmon"].lower()], int(g["ny"])
|
||||||
|
elif g.get("emon"): # April 29, 2026
|
||||||
|
d, mo, y = int(g["ed"]), _MONTHS[g["emon"].lower()], int(g["ey"])
|
||||||
|
else:
|
||||||
|
return None
|
||||||
return date(y, mo, d)
|
return date(y, mo, d)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError, KeyError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,16 @@ def test_kaufdatum_aus_beleg():
|
|||||||
assert guess_acquired_on("nichts hier") is None
|
assert guess_acquired_on("nichts hier") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_kaufdatum_amazon_monatsname():
|
||||||
|
# Amazon nennt das Kaufdatum als "29 April 2026" (Monatsname statt 29.04.).
|
||||||
|
text = "Bestelldatum 29 April 2026\nBestellnummer 028-6796274-2062709"
|
||||||
|
assert guess_acquired_on(text) == date(2026, 4, 29)
|
||||||
|
|
||||||
|
|
||||||
|
def test_kaufdatum_englischer_monatsname():
|
||||||
|
assert guess_acquired_on("Invoice date: April 29, 2026") == date(2026, 4, 29)
|
||||||
|
|
||||||
|
|
||||||
def test_shop_erkennt_bekannten_namen():
|
def test_shop_erkennt_bekannten_namen():
|
||||||
shops = [(1, "Digitec"), (2, "Galaxus")]
|
shops = [(1, "Digitec"), (2, "Galaxus")]
|
||||||
assert guess_shop("Rechnung von Digitec AG, Zürich", shops) == (1, "Digitec")
|
assert guess_shop("Rechnung von Digitec AG, Zürich", shops) == (1, "Digitec")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useConfirm } from "../confirm";
|
|||||||
import { useToast } from "../toast";
|
import { useToast } from "../toast";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { REASONS } from "./ObjektBestand";
|
import { REASONS } from "./ObjektBestand";
|
||||||
|
import { locationOptions, locationPathById } from "../locationPath";
|
||||||
|
|
||||||
const reasonLabel = (v) => REASONS.find((r) => r.value === v)?.label || v;
|
const reasonLabel = (v) => REASONS.find((r) => r.value === v)?.label || v;
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
const [docFile, setDocFile] = useState(null); // Beleg, der beim Anlegen mitkommt
|
const [docFile, setDocFile] = useState(null); // Beleg, der beim Anlegen mitkommt
|
||||||
const [newShopName, setNewShopName] = useState(null); // erkannter, noch nicht hinterlegter Shop
|
const [newShopName, setNewShopName] = useState(null); // erkannter, noch nicht hinterlegter Shop
|
||||||
const [addInfo, setAddInfo] = useState(null); // Hinweis nach Beleg-Analyse
|
const [addInfo, setAddInfo] = useState(null); // Hinweis nach Beleg-Analyse
|
||||||
|
const [priceCandidates, setPriceCandidates] = useState([]); // Preis-Vorschläge aus dem Beleg
|
||||||
const origin = window.location.origin;
|
const origin = window.location.origin;
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -77,6 +79,9 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
if (s.suggested_warranty_until) patch.warranty_until = s.suggested_warranty_until;
|
if (s.suggested_warranty_until) patch.warranty_until = s.suggested_warranty_until;
|
||||||
if (s.suggested_price_cents != null) { patch.price = (s.suggested_price_cents / 100).toFixed(2); patch.currency = "CHF"; }
|
if (s.suggested_price_cents != null) { patch.price = (s.suggested_price_cents / 100).toFixed(2); patch.currency = "CHF"; }
|
||||||
if (s.suggested_shop_id != null) patch.shop_id = String(s.suggested_shop_id);
|
if (s.suggested_shop_id != null) patch.shop_id = String(s.suggested_shop_id);
|
||||||
|
// Alle erkannten Preise anbieten (nicht nur den einen „wahrscheinlichsten") –
|
||||||
|
// so kann man beim Anlegen den richtigen Zeilenpreis wählen.
|
||||||
|
setPriceCandidates(s.suggested_price_candidates || []);
|
||||||
setAddForm((f) => ({ ...f, ...patch }));
|
setAddForm((f) => ({ ...f, ...patch }));
|
||||||
if (s.suggested_shop_id == null && s.suggested_shop_name) setNewShopName(s.suggested_shop_name);
|
if (s.suggested_shop_id == null && s.suggested_shop_name) setNewShopName(s.suggested_shop_name);
|
||||||
const teile = [];
|
const teile = [];
|
||||||
@@ -116,6 +121,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
setDocFile(null);
|
setDocFile(null);
|
||||||
setNewShopName(null);
|
setNewShopName(null);
|
||||||
setAddInfo(null);
|
setAddInfo(null);
|
||||||
|
setPriceCandidates([]);
|
||||||
await nachAktion();
|
await nachAktion();
|
||||||
toast("Einzelstück(e) angelegt.");
|
toast("Einzelstück(e) angelegt.");
|
||||||
} catch (err) { onError(err.message); }
|
} catch (err) { onError(err.message); }
|
||||||
@@ -237,7 +243,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
w.document.close();
|
w.document.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
const ortName = (id) => (id == null ? "" : locations.find((l) => l.id === id)?.name || "");
|
const ortName = (id) => locationPathById(id, locations);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
@@ -271,7 +277,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
<select value={it.location_id ?? ""} style={{ marginTop: 0, minWidth: 120 }}
|
<select value={it.location_id ?? ""} style={{ marginTop: 0, minWidth: 120 }}
|
||||||
onChange={(e) => patchItem(it, { location_id: e.target.value === "" ? null : e.target.value })}>
|
onChange={(e) => patchItem(it, { location_id: e.target.value === "" ? null : e.target.value })}>
|
||||||
<option value="">– ohne –</option>
|
<option value="">– ohne –</option>
|
||||||
{locations.map((l) => <option key={l.id} value={l.id}>{l.name}</option>)}
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
) : <span>{ortName(it.location_id) || "–"}</span>}
|
) : <span>{ortName(it.location_id) || "–"}</span>}
|
||||||
</td>
|
</td>
|
||||||
@@ -431,7 +437,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
<select value={addForm.location_id}
|
<select value={addForm.location_id}
|
||||||
onChange={(e) => setAddForm({ ...addForm, location_id: e.target.value })}>
|
onChange={(e) => setAddForm({ ...addForm, location_id: e.target.value })}>
|
||||||
<option value="">– ohne –</option>
|
<option value="">– ohne –</option>
|
||||||
{locations.map((l) => <option key={l.id} value={l.id}>{l.name}</option>)}
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="grow">Gekauft bei
|
<label className="grow">Gekauft bei
|
||||||
@@ -468,6 +474,17 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
{priceCandidates.length > 1 && (
|
||||||
|
<label style={{ maxWidth: 220 }}>Vorschlag aus Beleg
|
||||||
|
<select value={centsFrom(addForm.price) ?? ""} style={{ marginTop: 0 }}
|
||||||
|
onChange={(e) => setAddForm({ ...addForm, price: e.target.value === "" ? "" : (Number(e.target.value) / 100).toFixed(2) })}>
|
||||||
|
{!priceCandidates.includes(centsFrom(addForm.price)) && <option value="">– eigener –</option>}
|
||||||
|
{priceCandidates.map((c) => (
|
||||||
|
<option key={c} value={c}>{(c / 100).toFixed(2)} {addForm.currency}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<label className="grow">Beleg (Rechnung/Garantieschein)
|
<label className="grow">Beleg (Rechnung/Garantieschein)
|
||||||
<input type="file" accept="application/pdf,image/*"
|
<input type="file" accept="application/pdf,image/*"
|
||||||
onChange={(e) => { const f = e.target.files?.[0]; if (f) pickDoc(f); }} />
|
onChange={(e) => { const f = e.target.files?.[0]; if (f) pickDoc(f); }} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
import { locationOptions } from "../locationPath";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor für Mindestbestände je Lagerort (Bedarfe). Eine Zeile je Ort mit Menge;
|
* Editor für Mindestbestände je Lagerort (Bedarfe). Eine Zeile je Ort mit Menge;
|
||||||
@@ -58,7 +59,7 @@ export default function LocationMinStock({ locations, initial = [], unitLabel =
|
|||||||
<div className="field-inline" key={i} style={{ marginBottom: 0 }}>
|
<div className="field-inline" key={i} style={{ marginBottom: 0 }}>
|
||||||
<label className="grow" style={{ flex: "1 1 auto", minWidth: 0, margin: 0 }}>
|
<label className="grow" style={{ flex: "1 1 auto", minWidth: 0, margin: 0 }}>
|
||||||
<select value={r.location_id} onChange={(e) => setRow(i, { location_id: e.target.value })}>
|
<select value={r.location_id} onChange={(e) => setRow(i, { location_id: e.target.value })}>
|
||||||
{locations.map((l) => <option key={l.id} value={l.id}>{l.name}</option>)}
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label style={{ margin: 0, width: 130 }}>
|
<label style={{ margin: 0, width: 130 }}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useConfirm } from "../confirm";
|
|||||||
import { useToast } from "../toast";
|
import { useToast } from "../toast";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { fmt } from "../units";
|
import { fmt } from "../units";
|
||||||
|
import { locationOptions, locationPathById } from "../locationPath";
|
||||||
|
|
||||||
// Gründe zum Entfernen (Wert = Backend-Enum, Label = Anzeige).
|
// Gründe zum Entfernen (Wert = Backend-Enum, Label = Anzeige).
|
||||||
export const REASONS = [
|
export const REASONS = [
|
||||||
@@ -25,8 +26,7 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha
|
|||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const einheit = product?.unit_name || "Stück";
|
const einheit = product?.unit_name || "Stück";
|
||||||
const nameById = Object.fromEntries((locations || []).map((l) => [l.id, l.name]));
|
const ortName = (id) => (id == null ? "Ohne Lagerort" : locationPathById(id, locations) || `Ort ${id}`);
|
||||||
const ortName = (id) => (id == null ? "Ohne Lagerort" : nameById[id] || `Ort ${id}`);
|
|
||||||
|
|
||||||
const [addForm, setAddForm] = useState({ location_id: "", quantity: "" });
|
const [addForm, setAddForm] = useState({ location_id: "", quantity: "" });
|
||||||
const [move, setMove] = useState(null); // { from, to, quantity } | null
|
const [move, setMove] = useState(null); // { from, to, quantity } | null
|
||||||
@@ -185,7 +185,7 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha
|
|||||||
<select value={addForm.location_id}
|
<select value={addForm.location_id}
|
||||||
onChange={(e) => setAddForm({ ...addForm, location_id: e.target.value })}>
|
onChange={(e) => setAddForm({ ...addForm, location_id: e.target.value })}>
|
||||||
<option value="">– ohne Lagerort –</option>
|
<option value="">– ohne Lagerort –</option>
|
||||||
{locations.map((l) => <option key={l.id} value={l.id}>{l.name}</option>)}
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label style={{ width: 120 }}>
|
<label style={{ width: 120 }}>
|
||||||
@@ -210,13 +210,13 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha
|
|||||||
<label className="grow">Von
|
<label className="grow">Von
|
||||||
<select value={move.from} onChange={(e) => setMove({ ...move, from: e.target.value })}>
|
<select value={move.from} onChange={(e) => setMove({ ...move, from: e.target.value })}>
|
||||||
<option value="">– ohne Lagerort –</option>
|
<option value="">– ohne Lagerort –</option>
|
||||||
{locations.map((l) => <option key={l.id} value={l.id}>{l.name}</option>)}
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="grow">Nach
|
<label className="grow">Nach
|
||||||
<select value={move.to} onChange={(e) => setMove({ ...move, to: e.target.value })}>
|
<select value={move.to} onChange={(e) => setMove({ ...move, to: e.target.value })}>
|
||||||
<option value="">– ohne Lagerort –</option>
|
<option value="">– ohne Lagerort –</option>
|
||||||
{locations.map((l) => <option key={l.id} value={l.id}>{l.name}</option>)}
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label style={{ width: 110 }}>Menge
|
<label style={{ width: 110 }}>Menge
|
||||||
@@ -240,7 +240,7 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha
|
|||||||
<select value={remove.location_id}
|
<select value={remove.location_id}
|
||||||
onChange={(e) => setRemove({ ...remove, location_id: e.target.value })}>
|
onChange={(e) => setRemove({ ...remove, location_id: e.target.value })}>
|
||||||
<option value="">– ohne Lagerort –</option>
|
<option value="">– ohne Lagerort –</option>
|
||||||
{locations.map((l) => <option key={l.id} value={l.id}>{l.name}</option>)}
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label style={{ width: 110 }}>Menge
|
<label style={{ width: 110 }}>Menge
|
||||||
|
|||||||
36
web/src/locationPath.js
Normal file
36
web/src/locationPath.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Lagerorte sind verschachtelbar (Hedingen → Keller → Schublade 1). Gleichnamige
|
||||||
|
// Orte ("Schublade 1" an mehreren Stellen) sind nur am Pfad unterscheidbar –
|
||||||
|
// deshalb zeigen die Auswahlfelder den ganzen Pfad statt nur den Namen.
|
||||||
|
|
||||||
|
/** Pfad eines Lagerorts als "Hedingen → Keller → Schublade 1". */
|
||||||
|
export function locationPath(loc, all) {
|
||||||
|
if (!loc) return "";
|
||||||
|
const byId = new Map(all.map((l) => [l.id, l]));
|
||||||
|
const teile = [loc.name];
|
||||||
|
const gesehen = new Set([loc.id]);
|
||||||
|
let pid = loc.parent_id;
|
||||||
|
while (pid != null && byId.has(pid) && !gesehen.has(pid)) {
|
||||||
|
const p = byId.get(pid);
|
||||||
|
teile.unshift(p.name);
|
||||||
|
gesehen.add(pid);
|
||||||
|
pid = p.parent_id;
|
||||||
|
}
|
||||||
|
return teile.join(" → ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pfad zu einer Lagerort-ID (oder "" wenn unbekannt/leer). */
|
||||||
|
export function locationPathById(id, all) {
|
||||||
|
if (id == null || id === "") return "";
|
||||||
|
const loc = all.find((l) => l.id === id);
|
||||||
|
return loc ? locationPath(loc, all) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optionen fürs <select>: {id, label} mit vollem Pfad, nach Pfad sortiert –
|
||||||
|
* so stehen Geschwister und Unterorte beieinander.
|
||||||
|
*/
|
||||||
|
export function locationOptions(all) {
|
||||||
|
return (all || [])
|
||||||
|
.map((l) => ({ id: l.id, label: locationPath(l, all) }))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label, "de"));
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { useToast } from "../toast";
|
|||||||
import { suggestionToProduct } from "../offUtils";
|
import { suggestionToProduct } from "../offUtils";
|
||||||
import { asTree } from "../categoryTree";
|
import { asTree } from "../categoryTree";
|
||||||
import CategorySelect from "../components/CategorySelect";
|
import CategorySelect from "../components/CategorySelect";
|
||||||
|
import { locationOptions } from "../locationPath";
|
||||||
import { buildUnitOptions, fmt, fromMonthInput, isExpired, monthInputToLastDay } from "../units";
|
import { buildUnitOptions, fmt, fromMonthInput, isExpired, monthInputToLastDay } from "../units";
|
||||||
|
|
||||||
const emptyLine = () => ({ quantity: "", best_before: "" });
|
const emptyLine = () => ({ quantity: "", best_before: "" });
|
||||||
@@ -378,7 +379,7 @@ export default function CheckIn() {
|
|||||||
Lagerort (optional)
|
Lagerort (optional)
|
||||||
<select value={locationId} onChange={(e) => setLocationId(e.target.value)}>
|
<select value={locationId} onChange={(e) => setLocationId(e.target.value)}>
|
||||||
<option value="">– keiner –</option>
|
<option value="">– keiner –</option>
|
||||||
{locations.map((l) => <option key={l.id} value={l.id}>{l.name}</option>)}
|
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user