Karte 10: Kamera-Scan im Frontend (Objekt-Code, Scan-First-Einstieg)
CI / backend-tests (push) Successful in 1m0s
CI / frontend-build (push) Successful in 18s

BarcodeScanner-Komponente auf @zxing/browser (Code128, iOS/Safari-kompatibel
im Gegensatz zur nativen BarcodeDetector-API). Treffer springt direkt ins
Kontroll-Screen, sonst Fallback ins bestehende Such-Textfeld.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-04 17:39:24 +02:00
co-authored by Claude Sonnet 5
parent f06d704168
commit 6cf28cc8ee
6 changed files with 151 additions and 12 deletions
+36 -12
View File
@@ -1,17 +1,20 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import { apiRequest } from "../api/client";
import { apiRequest, ApiError } from "../api/client";
import type { Objekt } from "../api/types";
import { BarcodeScanner } from "../components/BarcodeScanner";
// Prompt 11 Screen 1: Standort/Objekt wählen. QR-/Barcode-Scan (Karte 10) ist
// Roadmap - dieser Screen bietet bewusst schon die Code-Suche als Textfeld an,
// ein späterer Kamera-Scan kann denselben Code einfach in dasselbe Feld schreiben.
// Prompt 11 Screen 1: Standort/Objekt wählen. Objekt-Code (Karte 10)
// Direktsprung ins Kontroll-Screen, sonst füllt der Scan 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);
useEffect(() => {
apiRequest<Objekt[]>("/objekte")
@@ -20,6 +23,20 @@ export function ObjektListPage() {
.finally(() => setLaedt(false));
}, []);
async function scanVerarbeiten(code: string) {
setScannerOffen(false);
try {
const objekt = await apiRequest<Objekt>(`/objekte/code/${encodeURIComponent(code)}`);
navigate(`/objekte/${objekt.id}/kontrolle`);
} 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()) ||
@@ -29,13 +46,20 @@ export function ObjektListPage() {
return (
<main className="page">
<h1>Objekte</h1>
<input
className="input input-search"
placeholder="Suche oder Code scannen…"
value={suche}
onChange={(e) => setSuche(e.target.value)}
style={{ marginBottom: "1.25rem" }}
/>
<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 && (