Lagerorte per 10-Zeichen-Code statt fortlaufender ID; iOS-Einlagern ohne Kamerazwang

Lagerort-IDs sind jetzt ein zufaelliger 10-Zeichen-Code (wie die Einzelstueck-UIDs) statt einer fortlaufenden Zahl - so kollidiert die Stammdaten-Sicherung zwischen zwei Instanzen praktisch nie mehr, und der Code ist zugleich der Inhalt des QR /l/<code>. Alle Fremdschluessel (lots, movements, items, Mindestbestaende, parent_id) ziehen mit; die Umstellung laeuft einmalig und transaktional beim Serverstart (_migrate_locations_to_code) und rollt bei Fehlern komplett zurueck. Vor dem Deploy ein DB-Backup machen.

iOS-Einlagern oeffnet nicht mehr sofort die Kamera, sondern ein Formular mit Artikelsuche; die Kamera kommt erst per Button. Im Formular laesst sich der Lagerort zusaetzlich per /l/-QR scannen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 12:15:25 +02:00
parent 6e48cd0a7a
commit d518b4aa23
23 changed files with 507 additions and 220 deletions

View File

@@ -100,7 +100,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
const cents = centsFrom(addForm.price);
const created = await api.createItems(product.id, {
count: Number(addForm.count) || 1,
location_id: addForm.location_id === "" ? null : Number(addForm.location_id),
location_id: addForm.location_id === "" ? null : addForm.location_id,
shop_id: shopId,
acquired_on: addForm.acquired_on || null,
warranty_until: addForm.warranty_until || null,
@@ -269,7 +269,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
<td data-label="Lagerort">
{isAdmin ? (
<select value={it.location_id ?? ""} style={{ marginTop: 0, minWidth: 120 }}
onChange={(e) => patchItem(it, { location_id: e.target.value === "" ? null : Number(e.target.value) })}>
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>)}
</select>

View File

@@ -35,7 +35,7 @@ export default function LocationMinStock({ locations, initial = [], unitLabel =
try {
const list = rows
.filter((r) => r.location_id !== "" && Number(r.min_stock) > 0)
.map((r) => ({ location_id: Number(r.location_id), min_stock: Number(r.min_stock) }));
.map((r) => ({ location_id: r.location_id, min_stock: Number(r.min_stock) }));
await onSave(list);
setOk(true);
} catch (err) {

View File

@@ -56,7 +56,7 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha
product_id: product.id,
quantity: menge,
unit: einheit,
location_id: addForm.location_id === "" ? null : Number(addForm.location_id),
location_id: addForm.location_id === "" ? null : addForm.location_id,
});
setAddForm({ location_id: addForm.location_id, quantity: "" });
await nachAktion();
@@ -93,8 +93,8 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha
await api.relocateStock({
product_id: product.id,
quantity: menge,
from_location_id: move.from === "" ? null : Number(move.from),
to_location_id: move.to === "" ? null : Number(move.to),
from_location_id: move.from === "" ? null : move.from,
to_location_id: move.to === "" ? null : move.to,
});
setMove(null);
toast("Umgelagert.");
@@ -110,7 +110,7 @@ export default function ObjektBestand({ product, lots, locations, isAdmin, onCha
await api.removeStock({
product_id: product.id,
quantity: menge,
location_id: remove.location_id === "" ? null : Number(remove.location_id),
location_id: remove.location_id === "" ? null : remove.location_id,
reason: remove.reason,
note: remove.note || null,
});

View File

@@ -237,7 +237,7 @@ export default function CheckIn() {
best_before:
(precision === "month" ? fromMonthInput(l.best_before) : l.best_before) || null,
best_before_precision: precision,
location_id: locationId === "" ? null : Number(locationId),
location_id: locationId === "" ? null : locationId,
}));
if (payloadLines.length === 0) {
setError("Bitte mindestens eine Menge angeben.");

View File

@@ -26,7 +26,7 @@ export default function LocationResolve() {
<div>
<div className="page-head">
<div>
<h1>Lagerort {name || `#${id}`}</h1>
<h1>Lagerort {name || id}</h1>
<div className="sub">
{error || "Diesen QR-Code in der App scannen (Ort zuordnen), um Einzelstücke hier einzuordnen."}
</div>

View File

@@ -30,7 +30,7 @@ export default function Locations() {
e.preventDefault();
setError(null);
try {
await api.createLocation({ name, parent_id: parentId === "" ? null : Number(parentId) });
await api.createLocation({ name, parent_id: parentId === "" ? null : parentId });
setName("");
// Übergeordneten Ort absichtlich stehen lassen beim Anlegen vieler Orte
// unter demselben Elternteil spart das jedes Mal die Neuauswahl.
@@ -56,7 +56,7 @@ export default function Locations() {
try {
await api.updateLocation(editId, {
name: editName.trim(),
parent_id: editParent == null ? null : Number(editParent),
parent_id: editParent == null ? null : editParent,
});
setEditId(null);
load();
@@ -141,7 +141,7 @@ export default function Locations() {
<label className="grow">
Übergeordneter Lagerort (optional)
<CategorySelect
value={parentId === "" ? null : Number(parentId)}
value={parentId === "" ? null : parentId}
nodes={ordered}
rootLabel=" keiner (oberste Ebene) "
onChange={(id) => setParentId(id == null ? "" : String(id))}