_synchronisiere_fehlbestand suchte nur nach Fehlbeständen der eigenen
Kontroll-Sitzung. Ein bereits offener Fehlbestand aus einer früheren
Kontrolle für dasselbe Objekt+Material wurde nicht wiedergefunden -
es entstand ein zweiter, paralleler Fehlbestand mit eigener (neuerer)
Sollmenge, der alte blieb mit veralteter Sollmenge offen liegen.
Lookup jetzt über objekt_id+material_id+status=offen statt
kontrolle_id; kontrolle_id am gefundenen Datensatz bleibt unverändert,
damit ein Abbruch der aktuellen Kontrolle ihn nicht fälschlich
mitlöscht. Löst sich die Abweichung über eine andere Kontrolle als
die erzeugende auf, wird der Fehlbestand jetzt sauber als erledigt
markiert (inkl. Mindermenge-Abschluss) statt stillschweigend gelöscht.
Nebenbei: Kommentare mit Kachel-/Datums-Referenzen ("Bug-Fund
2026-09-06", "UI-00X (Epic 20)") aus dem Code dieser Session entfernt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
|
|
|
import { apiRequest, ApiError } from "../api/client";
|
|
import type { Objekt } from "../api/types";
|
|
import { BarcodeScanner } from "../components/BarcodeScanner";
|
|
|
|
// Objekt-Code-Scan springt direkt zur Akte, sonst füllt er dasselbe Textfeld
|
|
// wie die manuelle Suche (Kamera fällt bei Etikettschaden auf Tippen zurück).
|
|
export function ObjektListPage() {
|
|
const navigate = useNavigate();
|
|
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
|
const [suche, setSuche] = useState("");
|
|
const [ladefehler, setLadefehler] = useState<string | null>(null);
|
|
const [laedt, setLaedt] = useState(true);
|
|
const [scannerOffen, setScannerOffen] = useState(false);
|
|
const [suchparameter, setSuchparameter] = useSearchParams();
|
|
|
|
// Quick Action "QR scannen" aus der Command Palette springt
|
|
// hierher mit ?scan=1 und öffnet den Scanner direkt, statt nur die Liste zu zeigen.
|
|
useEffect(() => {
|
|
if (suchparameter.get("scan") === "1") {
|
|
setScannerOffen(true);
|
|
setSuchparameter({}, { replace: true });
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
apiRequest<Objekt[]>("/objekte")
|
|
.then(setObjekte)
|
|
.catch(() => setLadefehler("Objektliste konnte nicht geladen werden."))
|
|
.finally(() => setLaedt(false));
|
|
}, []);
|
|
|
|
async function scanVerarbeiten(code: string) {
|
|
setScannerOffen(false);
|
|
try {
|
|
const objekt = await apiRequest<Objekt>(`/objekte/code/${encodeURIComponent(code)}`);
|
|
navigate(`/akte/objekt/${objekt.id}`);
|
|
} catch (err) {
|
|
if (err instanceof ApiError && err.status === 404) {
|
|
setSuche(code);
|
|
} else {
|
|
setLadefehler("Scan konnte nicht verarbeitet werden.");
|
|
}
|
|
}
|
|
}
|
|
|
|
const gefiltert = objekte.filter(
|
|
(o) =>
|
|
o.name.toLowerCase().includes(suche.toLowerCase()) ||
|
|
o.code.toLowerCase().includes(suche.toLowerCase())
|
|
);
|
|
|
|
return (
|
|
<main className="page">
|
|
<h1>Objekte</h1>
|
|
<div className="row" style={{ marginBottom: "1.25rem" }}>
|
|
<input
|
|
className="input input-search"
|
|
placeholder="Suche oder Code…"
|
|
value={suche}
|
|
onChange={(e) => setSuche(e.target.value)}
|
|
/>
|
|
<button className="btn btn-secondary" onClick={() => setScannerOffen(true)}>
|
|
Scannen
|
|
</button>
|
|
</div>
|
|
{scannerOffen && (
|
|
<BarcodeScanner onErkannt={scanVerarbeiten} onSchliessen={() => setScannerOffen(false)} />
|
|
)}
|
|
{ladefehler && <div className="alert alert-danger">{ladefehler}</div>}
|
|
{laedt && <p className="text-muted">Lade…</p>}
|
|
{!laedt && !ladefehler && gefiltert.length === 0 && (
|
|
<p className="text-muted">
|
|
{objekte.length === 0 ? "Noch keine Objekte angelegt." : "Keine Treffer."}
|
|
</p>
|
|
)}
|
|
<ul className="card-list">
|
|
{gefiltert.map((objekt) => (
|
|
<li key={objekt.id}>
|
|
<Link to={`/objekte/${objekt.id}/kontrolle`} className="card card-link">
|
|
<div className="card-title-row">
|
|
<strong>{objekt.name}</strong>
|
|
<span className="badge badge-neutral">{objekt.status}</span>
|
|
</div>
|
|
<div className="card-subtitle">{objekt.code}</div>
|
|
<div className="text-muted" style={{ fontSize: "0.85rem" }}>
|
|
{objekt.letzte_kontrolle_am
|
|
? `Letzte Kontrolle: ${new Date(objekt.letzte_kontrolle_am).toLocaleDateString("de-DE")}`
|
|
: "Noch nie kontrolliert"}
|
|
</div>
|
|
</Link>
|
|
<Link
|
|
to={`/akte/objekt/${objekt.id}`}
|
|
className="text-muted"
|
|
style={{ fontSize: "0.85rem" }}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
Akte ansehen
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</main>
|
|
);
|
|
}
|