diff --git a/package.json b/package.json index dd447eb..dc4115a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^5.5.0" + "react-icons": "^5.5.0", + "@zxing/browser": "^0.1.5" }, "devDependencies": { "@vitejs/plugin-react": "^4.5.1", @@ -21,4 +22,4 @@ "vite": "^5.2.0", "vite-plugin-pwa": "^1.0.0" } -} +} \ No newline at end of file diff --git a/src/components/BarcodeScanner.jsx b/src/components/BarcodeScanner.jsx new file mode 100644 index 0000000..a7f3b88 --- /dev/null +++ b/src/components/BarcodeScanner.jsx @@ -0,0 +1,132 @@ +import { useEffect, useRef, useState } from 'react'; +import { FaTimes, FaCamera, FaSyncAlt } from 'react-icons/fa'; + +// Scanner-Modal: oeffnet Kamera (bevorzugt Rueckkamera), liest Codes, +// liefert beim ersten Treffer den Text via onScan zurueck und schliesst sich. +export default function BarcodeScanner({ open, onClose, onScan }) { + const videoRef = useRef(null); + const controlsRef = useRef(null); + const [error, setError] = useState(''); + const [devices, setDevices] = useState([]); + const [deviceIdx, setDeviceIdx] = useState(0); + const [busy, setBusy] = useState(false); + + useEffect(() => { + if (!open) return; + let cancelled = false; + setError(''); + setBusy(true); + + (async () => { + try { + // Lade ZXing dynamisch -> reduziert initial-bundle + const { BrowserMultiFormatReader } = await import('@zxing/browser'); + if (cancelled) return; + const reader = new BrowserMultiFormatReader(); + + // Verfuegbare Kameras (fuer Switch-Button) + const cams = await BrowserMultiFormatReader.listVideoInputDevices(); + if (cancelled) return; + setDevices(cams); + + // Bevorzuge Rueckkamera ('back', 'environment') + let startDeviceId; + if (cams.length > 0) { + const backCam = cams.find(c => /back|rear|environment|umgebung/i.test(c.label)); + startDeviceId = (backCam || cams[cams.length - 1]).deviceId; + setDeviceIdx(cams.findIndex(c => c.deviceId === startDeviceId)); + } + + const controls = await reader.decodeFromVideoDevice( + startDeviceId, + videoRef.current, + (result, err, ctrl) => { + if (result) { + const text = result.getText(); + ctrl.stop(); + onScan(text); + } + } + ); + controlsRef.current = controls; + setBusy(false); + } catch (e) { + console.error('Scanner-Fehler:', e); + if (!cancelled) { + setError( + e?.name === 'NotAllowedError' + ? 'Kamerazugriff verweigert. Bitte in den Browser-Einstellungen erlauben.' + : e?.message || 'Kamera konnte nicht gestartet werden.' + ); + setBusy(false); + } + } + })(); + + return () => { + cancelled = true; + try { controlsRef.current?.stop(); } catch {} + }; + }, [open, onScan]); + + const switchCamera = async () => { + if (devices.length < 2) return; + try { controlsRef.current?.stop(); } catch {} + const next = (deviceIdx + 1) % devices.length; + setDeviceIdx(next); + setBusy(true); + const { BrowserMultiFormatReader } = await import('@zxing/browser'); + const reader = new BrowserMultiFormatReader(); + const controls = await reader.decodeFromVideoDevice( + devices[next].deviceId, videoRef.current, + (result, err, ctrl) => { if (result) { ctrl.stop(); onScan(result.getText()); } } + ); + controlsRef.current = controls; + setBusy(false); + }; + + if (!open) return null; + + return ( +
+
+
+
+ +

Barcode scannen

+
+
+ {devices.length > 1 && ( + + )} + +
+
+ +
+
+
+ ); +} diff --git a/src/components/Geraete.jsx b/src/components/Geraete.jsx index 6a3c970..c9ce849 100644 --- a/src/components/Geraete.jsx +++ b/src/components/Geraete.jsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; -import { FaPlus, FaEdit, FaTrash, FaTimes } from 'react-icons/fa'; +import { FaPlus, FaEdit, FaTrash, FaTimes, FaQrcode } from 'react-icons/fa'; +import BarcodeScanner from './BarcodeScanner'; import { api } from '../api/client'; const STATUS = { @@ -20,6 +21,7 @@ export default function Geräte() { const [loading, setLoading] = useState(true); const [filterTyp, setFilterTyp] = useState(''); const [filterStatus, setFilterStatus] = useState(''); + const [scannerOpen, setScannerOpen] = useState(false); const load = async () => { setLoading(true); @@ -62,7 +64,14 @@ export default function Geräte() {

{editId ? 'Gerät bearbeiten' : 'Neues Gerät'}

- setForm(f => ({ ...f, seriennummer: e.target.value }))} /> +
+ setForm(f => ({ ...f, seriennummer: e.target.value }))} /> + +
)}
+ setScannerOpen(false)} + onScan={(code) => { + setForm(f => ({ ...f, seriennummer: code })); + setScannerOpen(false); + }} />
); }