Web: Lagerort-Inhalt ansehen + von dort einlagern
Der Lagerort-QR (/l/<code>) zeigt jetzt, was am Ort (inkl. Unterorte) liegt - Artikel mit Bestand, jeweils verlinkt - statt nur den Namen. "Hier einlagern" springt ins Einlagern mit vorbelegtem Lagerort (?location=). In der Lagerorte- Verwaltung fuehrt ein neues Lupensymbol zur Inhaltsansicht. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -231,6 +231,8 @@ export const api = {
|
|||||||
|
|
||||||
// Stammdaten
|
// Stammdaten
|
||||||
listLocations: () => request("/locations"),
|
listLocations: () => request("/locations"),
|
||||||
|
// Was liegt an einem Lagerort (inkl. Unterorte)? Ziel des Lagerort-QR.
|
||||||
|
locationContents: (code) => request(`/location/${encodeURIComponent(code)}`),
|
||||||
createLocation: (body) => request("/locations", { method: "POST", body }),
|
createLocation: (body) => request("/locations", { method: "POST", body }),
|
||||||
updateLocation: (id, body) => request(`/locations/${id}`, { method: "PATCH", body }),
|
updateLocation: (id, body) => request(`/locations/${id}`, { method: "PATCH", body }),
|
||||||
deleteLocation: (id) => request(`/locations/${id}`, { method: "DELETE" }),
|
deleteLocation: (id) => request(`/locations/${id}`, { method: "DELETE" }),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useSearchParams } from "react-router-dom";
|
||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import { useAuth } from "../auth";
|
import { useAuth } from "../auth";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
@@ -43,6 +43,7 @@ export default function CheckIn() {
|
|||||||
|
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.listLocations().then(setLocations).catch(() => {});
|
api.listLocations().then(setLocations).catch(() => {});
|
||||||
@@ -51,6 +52,12 @@ export default function CheckIn() {
|
|||||||
api.listUnits().then(setUnits).catch(() => {});
|
api.listUnits().then(setUnits).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Aus der Lagerort-Ansicht („Hier einlagern") den Ort vorbelegen.
|
||||||
|
useEffect(() => {
|
||||||
|
const loc = searchParams.get("location");
|
||||||
|
if (loc) setLocationId(loc);
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
function resetLookup() {
|
function resetLookup() {
|
||||||
setSuggestion(null);
|
setSuggestion(null);
|
||||||
setUnknownBarcode(null);
|
setUnknownBarcode(null);
|
||||||
|
|||||||
@@ -1,38 +1,82 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
|
import { ProduktThumb } from "../components/ProduktBild";
|
||||||
|
import { fmt } from "../units";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ziel eines gescannten Lagerort-QR (/l/<id>). Im Browser zeigt es nur den
|
* Ziel eines gescannten Lagerort-QR (/l/<code>) – zeigt, was an diesem Ort (und
|
||||||
* Namen; das eigentliche Zuordnen (Stück → Ort) läuft über die App.
|
* allen Unterorten) liegt, mit Bestand je Artikel. Von hier direkt einlagern.
|
||||||
*/
|
*/
|
||||||
export default function LocationResolve() {
|
export default function LocationResolve() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [name, setName] = useState(null);
|
const [data, setData] = useState(null); // { location_name, products: [...] }
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.listLocations()
|
setLoading(true);
|
||||||
.then((ls) => {
|
setError(null);
|
||||||
const l = ls.find((x) => String(x.id) === String(id));
|
api.locationContents(id)
|
||||||
if (l) setName(l.name);
|
.then(setData)
|
||||||
else setError("Lagerort nicht gefunden.");
|
.catch((e) => setError(e.message))
|
||||||
})
|
.finally(() => setLoading(false));
|
||||||
.catch((e) => setError(e.message));
|
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
const produkte = data?.products || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-head">
|
<div className="page-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>Lagerort {name || id}</h1>
|
<h1><Icon name="location" size={20} /> {data?.location_name || `Lagerort ${id}`}</h1>
|
||||||
<div className="sub">
|
<div className="sub">
|
||||||
{error || "Diesen QR-Code in der App scannen (Ort zuordnen), um Einzelstücke hier einzuordnen."}
|
{loading
|
||||||
|
? "Wird geladen…"
|
||||||
|
: `${produkte.length} Artikel hier (inkl. Unterorte)`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Link className="btn primary" to={`/checkin?location=${encodeURIComponent(id)}`}>
|
||||||
|
<Icon name="checkin" size={16} />Hier einlagern
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<div className="table-wrap">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="thumb-cell"></th>
|
||||||
|
<th>Artikel</th>
|
||||||
|
<th className="num">Bestand</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{produkte.map((p) => (
|
||||||
|
<tr key={p.product_id}>
|
||||||
|
<td className="thumb-cell"><ProduktThumb productId={p.product_id} alt={p.name} /></td>
|
||||||
|
<td>
|
||||||
|
<span className="strong">{p.name}</span>
|
||||||
|
{p.brand && <div className="muted small">{p.brand}</div>}
|
||||||
|
</td>
|
||||||
|
<td className="num">{fmt(p.stock)} {p.unit_label}</td>
|
||||||
|
<td className="num"><Link to={`/products/${p.product_id}`}>Details</Link></td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!loading && produkte.length === 0 && (
|
||||||
|
<tr><td colSpan={4} className="empty">Hier liegt gerade nichts.</td></tr>
|
||||||
|
)}
|
||||||
|
{loading && (
|
||||||
|
<tr><td colSpan={4} className="empty">Wird geladen…</td></tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import { useConfirm } from "../confirm";
|
import { useConfirm } from "../confirm";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
@@ -188,6 +189,9 @@ export default function Locations() {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
<Link className="btn-icon" to={`/l/${l.id}`} title="Inhalt ansehen">
|
||||||
|
<Icon name="search" size={16} />
|
||||||
|
</Link>
|
||||||
<QrImg text={`${window.location.origin}/l/${l.id}`} size={44} />
|
<QrImg text={`${window.location.origin}/l/${l.id}`} size={44} />
|
||||||
<button className="btn-icon" onClick={() => startEdit(l)} title="Bearbeiten">
|
<button className="btn-icon" onClick={() => startEdit(l)} title="Bearbeiten">
|
||||||
<Icon name="edit" size={16} />
|
<Icon name="edit" size={16} />
|
||||||
|
|||||||
Reference in New Issue
Block a user