diff --git a/DEVLOG.md b/DEVLOG.md index e2d61da..09885d8 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2578,3 +2578,16 @@ Keine Commits in dieser Session. - backend/tests/test_objekte.py | 83 ++++++++++++ --- +## 2026-09-04 17:37 – 17:37 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** frontend + +### Commits +- f06d704 Frontend: Code-Vorschlag beim Objekt-Anlegen/Duplizieren einbinden + +### Geänderte Dateien +- DEVLOG.md | 36 +++++++++++++++++++ +- frontend/DEVLOG.md | 20 +++++++++++ +- frontend/src/pages/admin/ObjektSection.tsx | 58 +++++++++++++++++++++++++++++- + +--- diff --git a/frontend/DEVLOG.md b/frontend/DEVLOG.md index e7f1ffb..f56f3b8 100644 --- a/frontend/DEVLOG.md +++ b/frontend/DEVLOG.md @@ -18,3 +18,16 @@ Keine Commits in dieser Session. - backend/tests/test_objekte.py | 83 ++++++++++++ --- +## 2026-09-04 17:37 – 17:38 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 36 +++++++++++++++++++ +- frontend/DEVLOG.md | 20 +++++++++++ +- frontend/src/pages/admin/ObjektSection.tsx | 58 +++++++++++++++++++++++++++++- + +--- diff --git a/frontend/package.json b/frontend/package.json index 121e3c9..93c418d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@zxing/browser": "^0.1.5", "idb-keyval": "^6.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/components/BarcodeScanner.tsx b/frontend/src/components/BarcodeScanner.tsx new file mode 100644 index 0000000..99d7681 --- /dev/null +++ b/frontend/src/components/BarcodeScanner.tsx @@ -0,0 +1,64 @@ +import { useEffect, useRef, useState } from "react"; + +interface Props { + onErkannt: (code: string) => void; + onSchliessen: () => void; +} + +/** + * Kamera-Scan (Karte 10): Code128-Etiketten sind kameraseitig empfindlicher als + * QR (Abstand/Winkel/Druckqualität) - deshalb bewusst kein Auto-Close bei jedem + * Treffer, sondern Fehler bleiben sichtbar, bis der Nutzer erneut versucht oder + * abbricht. @zxing/browser statt der nativen BarcodeDetector-API, weil Letztere + * auf iOS/Safari fehlt - Einsatzgeräte sind gemischt (Karte 06). + */ +export function BarcodeScanner({ onErkannt, onSchliessen }: Props) { + const videoRef = useRef(null); + const [fehler, setFehler] = useState(null); + + useEffect(() => { + let abgebrochen = false; + let controls: { stop: () => void } | null = null; + + async function starten() { + try { + const { BrowserMultiFormatReader } = await import("@zxing/browser"); + if (abgebrochen || !videoRef.current) return; + const reader = new BrowserMultiFormatReader(); + controls = await reader.decodeFromVideoDevice( + undefined, + videoRef.current, + (ergebnis, fehlerErgebnis) => { + if (ergebnis) { + onErkannt(ergebnis.getText()); + } else if (fehlerErgebnis && fehlerErgebnis.name !== "NotFoundException") { + setFehler("Scan-Fehler - bitte Kamera-Berechtigung prüfen."); + } + } + ); + } catch { + if (!abgebrochen) { + setFehler("Kamera konnte nicht gestartet werden - Berechtigung erteilt?"); + } + } + } + starten(); + + return () => { + abgebrochen = true; + controls?.stop(); + }; + }, [onErkannt]); + + return ( +
+
+
+
+ ); +} diff --git a/frontend/src/pages/ObjektListPage.tsx b/frontend/src/pages/ObjektListPage.tsx index 04f29be..1f3f2a0 100644 --- a/frontend/src/pages/ObjektListPage.tsx +++ b/frontend/src/pages/ObjektListPage.tsx @@ -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([]); const [suche, setSuche] = useState(""); const [ladefehler, setLadefehler] = useState(null); const [laedt, setLaedt] = useState(true); + const [scannerOffen, setScannerOffen] = useState(false); useEffect(() => { apiRequest("/objekte") @@ -20,6 +23,20 @@ export function ObjektListPage() { .finally(() => setLaedt(false)); }, []); + async function scanVerarbeiten(code: string) { + setScannerOffen(false); + try { + const objekt = await apiRequest(`/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 (

Objekte

- setSuche(e.target.value)} - style={{ marginBottom: "1.25rem" }} - /> +
+ setSuche(e.target.value)} + /> + +
+ {scannerOffen && ( + setScannerOffen(false)} /> + )} {ladefehler &&
{ladefehler}
} {laedt &&

Lade…

} {!laedt && !ladefehler && gefiltert.length === 0 && ( diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 200abee..ef2fff1 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -321,3 +321,27 @@ select.input:focus { .section { margin-bottom: 2rem; } + +.scanner-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.scanner-box { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 1rem; +} + +.scanner-video { + width: min(90vw, 480px); + border-radius: 0.5rem; + background: #000; +}