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:
@@ -5,6 +5,7 @@ import { useConfirm } from "../confirm";
|
||||
import { useToast } from "../toast";
|
||||
import Icon from "./Icon";
|
||||
import { REASONS } from "./ObjektBestand";
|
||||
import { locationOptions, locationPathById } from "../locationPath";
|
||||
|
||||
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 [newShopName, setNewShopName] = useState(null); // erkannter, noch nicht hinterlegter Shop
|
||||
const [addInfo, setAddInfo] = useState(null); // Hinweis nach Beleg-Analyse
|
||||
const [priceCandidates, setPriceCandidates] = useState([]); // Preis-Vorschläge aus dem Beleg
|
||||
const origin = window.location.origin;
|
||||
|
||||
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_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);
|
||||
// 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 }));
|
||||
if (s.suggested_shop_id == null && s.suggested_shop_name) setNewShopName(s.suggested_shop_name);
|
||||
const teile = [];
|
||||
@@ -116,6 +121,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
||||
setDocFile(null);
|
||||
setNewShopName(null);
|
||||
setAddInfo(null);
|
||||
setPriceCandidates([]);
|
||||
await nachAktion();
|
||||
toast("Einzelstück(e) angelegt.");
|
||||
} catch (err) { onError(err.message); }
|
||||
@@ -237,7 +243,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
||||
w.document.close();
|
||||
}
|
||||
|
||||
const ortName = (id) => (id == null ? "" : locations.find((l) => l.id === id)?.name || "");
|
||||
const ortName = (id) => locationPathById(id, locations);
|
||||
|
||||
return (
|
||||
<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 }}
|
||||
onChange={(e) => patchItem(it, { location_id: e.target.value === "" ? null : e.target.value })}>
|
||||
<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>
|
||||
) : <span>{ortName(it.location_id) || "–"}</span>}
|
||||
</td>
|
||||
@@ -431,7 +437,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
||||
<select value={addForm.location_id}
|
||||
onChange={(e) => setAddForm({ ...addForm, location_id: e.target.value })}>
|
||||
<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>
|
||||
</label>
|
||||
<label className="grow">Gekauft bei
|
||||
@@ -468,6 +474,17 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
||||
</select>
|
||||
</div>
|
||||
</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)
|
||||
<input type="file" accept="application/pdf,image/*"
|
||||
onChange={(e) => { const f = e.target.files?.[0]; if (f) pickDoc(f); }} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import Icon from "./Icon";
|
||||
import { locationOptions } from "../locationPath";
|
||||
|
||||
/**
|
||||
* 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 }}>
|
||||
<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 })}>
|
||||
{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>
|
||||
</label>
|
||||
<label style={{ margin: 0, width: 130 }}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useConfirm } from "../confirm";
|
||||
import { useToast } from "../toast";
|
||||
import Icon from "./Icon";
|
||||
import { fmt } from "../units";
|
||||
import { locationOptions, locationPathById } from "../locationPath";
|
||||
|
||||
// Gründe zum Entfernen (Wert = Backend-Enum, Label = Anzeige).
|
||||
export const REASONS = [
|
||||
@@ -25,8 +26,7 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
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" : nameById[id] || `Ort ${id}`);
|
||||
const ortName = (id) => (id == null ? "Ohne Lagerort" : locationPathById(id, locations) || `Ort ${id}`);
|
||||
|
||||
const [addForm, setAddForm] = useState({ location_id: "", quantity: "" });
|
||||
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}
|
||||
onChange={(e) => setAddForm({ ...addForm, location_id: e.target.value })}>
|
||||
<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>
|
||||
</label>
|
||||
<label style={{ width: 120 }}>
|
||||
@@ -210,13 +210,13 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha
|
||||
<label className="grow">Von
|
||||
<select value={move.from} onChange={(e) => setMove({ ...move, from: e.target.value })}>
|
||||
<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>
|
||||
</label>
|
||||
<label className="grow">Nach
|
||||
<select value={move.to} onChange={(e) => setMove({ ...move, to: e.target.value })}>
|
||||
<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>
|
||||
</label>
|
||||
<label style={{ width: 110 }}>Menge
|
||||
@@ -240,7 +240,7 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha
|
||||
<select value={remove.location_id}
|
||||
onChange={(e) => setRemove({ ...remove, location_id: e.target.value })}>
|
||||
<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>
|
||||
</label>
|
||||
<label style={{ width: 110 }}>Menge
|
||||
|
||||
Reference in New Issue
Block a user