diff --git a/README.md b/README.md index 2c3f9a2..7815f5c 100755 --- a/README.md +++ b/README.md @@ -50,18 +50,32 @@ Das Modul bietet drei Modi für die mobile Barcode-Erfassung: ## Barcode-Unterstützung -Das Modul sucht Barcodes in folgender Reihenfolge: +Das Modul sucht Barcodes/Artikelnummern in folgender Reihenfolge: 1. Produkt-Barcode (`llx_product.barcode`) 2. Lieferanten-Barcode (`llx_product_fournisseur_price.barcode`) -3. Produkt-Referenz (`llx_product.ref`) +3. **Lieferanten-Artikelnummer** (`llx_product_fournisseur_price.ref_fourn`) ← **NEU ab v9.0** +4. Produkt-Referenz (`llx_product.ref`) -Unterstützte Barcode-Formate (Quagga2): -- **Code 128** (empfohlen für alphanumerische Codes, höchste Priorität) -- **Code 39** (alphanumerisch, zweite Priorität) -- **EAN-13** (nur Ziffern, dritte Priorität) -- **EAN-8** (nur Ziffern, niedrigste Priorität) +### Barcode-Formate (ZXing-JS, ab v8.3) -**WICHTIG:** Reader-Reihenfolge optimiert für alphanumerische Codes! CODE128/CODE39 haben Vorrang vor EAN-Readern, um Fehlerkennungen bei Herstellercodes wie "P20260030" zu vermeiden. +**Unterstützte Formate - wählbar per Dropdown:** +- **Code 128** - alphanumerisch (1D-Barcode) +- **QR-Code** - 2D-Matrix-Code +- **DataMatrix** - 2D-Matrix-Code + +**Typ-Umschaltung ohne Kamera-Neustart:** +Der Barcode-Typ kann während des Scannens gewechselt werden. Die Kamera bleibt aktiv, nur der Decoder wird neu initialisiert. Die Auswahl wird persistent in localStorage gespeichert. + +### OCR-Modus für Artikelnummern (ab v8.5, optimiert in v9.1) + +**Für Etiketten OHNE Barcode:** +- Foto-basierte Texterkennung mit Tesseract.js +- Variable Ziffern-Anzahl (5-12 stellig) per Dropdown wählbar +- Erkennt **zusammenhängende** N-stellige Zahlen (z.B. 7-stellige Artikelnummer) +- Mehrere Treffer → Auswahl-Dialog +- Keine Treffer → Fallback auf manuelle Produktsuche + +**Wichtig:** OCR erkennt nur **isolierte** Zahlenblöcke mit exakt N Ziffern. Keine Subsequenzen oder Zusammensetzungen aus längeren Zahlen! ## Installation @@ -186,8 +200,9 @@ handybarcodescanner/ ``` ### Verwendete Bibliotheken -- [Quagga2](https://github.com/ericblade/quagga2) - Browser-basierte Barcode-Erkennung -- [JsBarcode](https://github.com/lindell/JsBarcode) - Barcode-Generierung für Druck +- [ZXing-JS](https://github.com/zxing-js/library) v0.21.3 - Browser-basierte Barcode-Erkennung (1D + 2D) +- [Tesseract.js](https://github.com/naptha/tesseract.js) v5.1.0 - OCR (Optical Character Recognition) für Artikelnummern +- [JsBarcode](https://github.com/lindell/JsBarcode) v3.11.6 - Barcode-Generierung für Druck - [ZXing](https://github.com/zxing/zxing) - Barcode-Generierung in Android App ### PWA (Progressive Web App) @@ -203,7 +218,15 @@ Das Modul ist als PWA konzipiert mit: - Firefox auf Android: Nur Shortcuts, kein echter Standalone-Modus ⚠️ - Safari auf iOS: PWA-Unterstützung mit Einschränkungen -**Versionsverwaltung:** Bei Updates CACHE_NAME in `sw.js` + `?v=` in `pwa.php` synchron erhöhen! +**Versionsverwaltung:** Bei Updates IMMER synchron erhöhen: +- `CACHE_NAME` in `sw.js` (z.B. `scanner-v9.1`) +- `?v=` Parameter in `pwa.php` für CSS und JS (z.B. `?v=91`) +- `$this->version` in `modHandyBarcodeScanner.class.php` (z.B. `9.1`) + +**Aktuelle Version: 9.1** +- v9.1: OCR-Feature Bug-Fix (extractDigitSequences: nur zusammenhängende Zahlenblöcke) +- v9.0: OCR-Feature für Artikelnummern-Erkennung +- v8.3: ZXing-JS Integration (Code 128, QR, DataMatrix) ### Brother PT-E560BT Android App diff --git a/android-app/BrotherPrintHelper-v2.apk b/android-app/BrotherPrintHelper-v2.apk new file mode 100755 index 0000000..f538458 Binary files /dev/null and b/android-app/BrotherPrintHelper-v2.apk differ diff --git a/core/modules/modHandyBarcodeScanner.class.php b/core/modules/modHandyBarcodeScanner.class.php index 37513b3..1e900cb 100755 --- a/core/modules/modHandyBarcodeScanner.class.php +++ b/core/modules/modHandyBarcodeScanner.class.php @@ -76,7 +76,7 @@ class modHandyBarcodeScanner extends DolibarrModules $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@handybarcodescanner' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '8.2'; + $this->version = '9.1'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; diff --git a/js/scanner.js b/js/scanner.js index 091a81f..e16f018 100755 --- a/js/scanner.js +++ b/js/scanner.js @@ -44,9 +44,18 @@ let currentProduct = null; let selectedSupplier = null; let allSuppliers = []; - let quaggaInitialized = false; + let scannerInitialized = false; let openDialogCount = 0; // Zaehlt offene Dialoge + // ZXing Scanner State + let currentBarcodeType = 'CODE_128'; // Aktuell gewahlter Barcode-Typ + let currentScanner = null; // ZXing BrowserMultiFormatReader instance + + // OCR State (Tesseract.js) + let ocrWorker = null; // Singleton Worker + let ocrInitialized = false; + let currentDigitCount = 8; // Anzahl Ziffern für OCR-Erkennung + // Persistente Einstellungen - eine zentrale Config statt einzelner Keys const STORAGE_KEY = 'hbs_config'; function loadConfig() { @@ -133,6 +142,49 @@ window._showProductSearchModal = showProductSearchModal; window._showFreetextModal = showFreetextModal; window._showOrdersPanel = showOrdersPanel; + + // History-Button Event-Handler + const historyBtn = document.getElementById('ctrl-history'); + if (historyBtn) { + historyBtn.addEventListener('click', showBarcodeHistory); + } + + // Barcode-Typ-Dropdown Event-Handler + const typeSelect = document.getElementById('barcode-type-select'); + if (typeSelect) { + // Gespeicherten Typ laden + const savedType = loadConfig().barcodeType || 'CODE_128'; + typeSelect.value = savedType; + currentBarcodeType = savedType; + + // Initiale Scan-Region-Farbe setzen + updateScanRegionColor(savedType); + + // OCR-Digit-Selector initial ein/ausblenden + const digitSelector = document.getElementById('ocr-digit-selector'); + if (digitSelector) { + digitSelector.classList.toggle('hidden', savedType !== 'OCR'); + } + + // Event-Listener fuer Typ-Wechsel + typeSelect.addEventListener('change', function() { + switchBarcodeType(this.value); + }); + } + + // OCR Digit-Count Dropdown Handler + const digitSelect = document.getElementById('ocr-digit-count'); + if (digitSelect) { + const savedCount = loadConfig().ocrDigitCount || 8; + digitSelect.value = savedCount; + currentDigitCount = savedCount; + + digitSelect.addEventListener('change', function() { + currentDigitCount = parseInt(this.value); + saveConfig('ocrDigitCount', currentDigitCount); + showToast('OCR: ' + currentDigitCount + ' Ziffern', 'success'); + }); + } } // Check camera support - just log warnings, never disable button @@ -143,9 +195,9 @@ console.warn('HandyBarcodeScanner: HTTPS empfohlen fuer Kamerazugriff'); } - // Check if Quagga is loaded - if (typeof Quagga === 'undefined') { - console.error('HandyBarcodeScanner: Quagga nicht geladen'); + // Check if ZXing is loaded + if (typeof ZXing === 'undefined') { + console.error('HandyBarcodeScanner: ZXing nicht geladen'); } // Check mediaDevices @@ -224,10 +276,17 @@ return; } - // Check if Quagga is available - if (typeof Quagga === 'undefined') { - showToast('Quagga Bibliothek nicht geladen', 'error'); - return; + // Check if libraries are available + if (currentBarcodeType === 'OCR') { + if (typeof Tesseract === 'undefined') { + showToast('Tesseract.js Bibliothek nicht geladen', 'error'); + return; + } + } else { + if (typeof ZXing === 'undefined') { + showToast('ZXing Bibliothek nicht geladen', 'error'); + return; + } } // Disable button while initializing @@ -240,8 +299,14 @@ // Permission granted - stop the test stream stream.getTracks().forEach(track => track.stop()); - // Now start Quagga - initQuagga(); + // OCR-Modus oder Barcode-Modus? + if (currentBarcodeType === 'OCR') { + // OCR-Modus: Kamera-Preview + "Foto aufnehmen" Button + startCameraPreview(); + } else { + // Barcode-Modus: ZXing Video-Stream + currentScanner = initZXing(currentBarcodeType); + } }) .catch(function(err) { console.error('Camera permission error:', err); @@ -260,68 +325,63 @@ }); } - function initQuagga() { - Quagga.init({ - inputStream: { - name: "Live", - type: "LiveStream", - target: elements.videoContainer, - constraints: { - facingMode: "environment", - width: { min: 640, ideal: 1280, max: 1920 }, - height: { min: 480, ideal: 720, max: 1080 } - }, - area: { // Scan-Bereich: groesserer Bereich fuer kleine Barcodes - top: "10%", - right: "5%", - left: "5%", - bottom: "10%" + function getBarcodeFormat(type) { + const formats = { + 'CODE_128': ZXing.BarcodeFormat.CODE_128, + 'QR_CODE': ZXing.BarcodeFormat.QR_CODE, + 'DATA_MATRIX': ZXing.BarcodeFormat.DATA_MATRIX, + 'CODE_39': ZXing.BarcodeFormat.CODE_39, + 'EAN_13': ZXing.BarcodeFormat.EAN_13, + 'EAN_8': ZXing.BarcodeFormat.EAN_8 + }; + return formats[type] || ZXing.BarcodeFormat.CODE_128; + } + + function initZXing(format) { + format = format || currentBarcodeType; + + try { + const hints = new Map(); + const possibleFormats = [getBarcodeFormat(format)]; + hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, possibleFormats); + + const codeReader = new ZXing.BrowserMultiFormatReader(hints); + + codeReader.decodeFromVideoDevice( + undefined, // Auto-select Kamera + elements.video, + (result, err) => { + if (result) { + onBarcodeDetected(result); + } + // Errors werden ignoriert (passiert bei jedem Frame ohne Code) } - }, - decoder: { - readers: [ - "code_128_reader", // Best for alphanumeric codes - "code_39_reader", // Also good for alphanumeric (e.g. P20260030) - "ean_reader", // Numeric only - lower priority - "ean_8_reader" // Numeric only - lower priority - ], - multiple: false - }, - locate: true, - locator: { - patchSize: "medium", // medium = besser fuer kleine Barcodes - halfSample: false // volle Aufloesung = bessere Genauigkeit - }, - numOfWorkers: navigator.hardwareConcurrency || 4, - frequency: 20 // haeufiger scannen fuer bessere Erkennung - }, function(err) { - if (err) { - console.error('Quagga init error:', err); + ).then(() => { + // Scanner erfolgreich gestartet + scannerInitialized = true; + isScanning = true; + elements.startBtn.disabled = false; + elements.startBtn.classList.add('hidden'); + elements.stopBtn.classList.remove('hidden'); + elements.videoContainer.classList.add('scanning'); + + // Timeout-Timer starten + startScanTimeout(); + }).catch((err) => { + console.error('ZXing init error:', err); elements.startBtn.disabled = false; elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten'; showToast(CONFIG.lang.cameraError || 'Kamerafehler', 'error'); - return; - } + }); - // Success - start scanning - Quagga.start(); - quaggaInitialized = true; - isScanning = true; + return codeReader; + } catch (e) { + console.error('ZXing initialization error:', e); elements.startBtn.disabled = false; - elements.startBtn.classList.add('hidden'); - elements.stopBtn.classList.remove('hidden'); - elements.videoContainer.classList.add('scanning'); - - // Quagga-Canvas darf keine Klicks abfangen - const canvases = elements.videoContainer.querySelectorAll('canvas'); - canvases.forEach(function(c) { c.style.pointerEvents = 'none'; }); - - // Register detection handler - Quagga.onDetected(onBarcodeDetected); - - // Timeout-Timer starten - startScanTimeout(); - }); + elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten'; + showToast('Scanner konnte nicht initialisiert werden', 'error'); + return null; + } } // Timeout-Hinweis wenn nichts erkannt wird @@ -339,18 +399,29 @@ function stopScanner() { clearTimeout(scanTimeoutTimer); - if (quaggaInitialized) { + // OCR-Modus: Video-Stream manuell stoppen + if (currentBarcodeType === 'OCR') { + const video = elements.video; + if (video && video.srcObject) { + video.srcObject.getTracks().forEach(track => track.stop()); + video.srcObject = null; + } + // Button-Click-Handler zurücksetzen + elements.startBtn.onclick = null; + } + + if (currentScanner && scannerInitialized) { try { - Quagga.offDetected(onBarcodeDetected); - Quagga.stop(); + currentScanner.reset(); } catch (e) { - console.log('Quagga stop error:', e); + console.log('Scanner stop error:', e); } } isScanning = false; isPaused = false; - quaggaInitialized = false; + scannerInitialized = false; + currentScanner = null; elements.startBtn.classList.remove('hidden'); elements.startBtn.disabled = false; elements.startBtn.textContent = CONFIG.lang.startScan || 'Scannen'; @@ -359,19 +430,12 @@ } // Scanner pausieren (bei Dialog-Oeffnung) + // ZXing hat kein natives pause() - wir nutzen ein Flag und ignorieren Detections function pauseScanner() { if (!isScanning || isPaused) return; openDialogCount++; - - if (quaggaInitialized) { - try { - Quagga.pause(); - isPaused = true; - console.log('Scanner paused, dialogs:', openDialogCount); - } catch (e) { - console.log('Quagga pause error:', e); - } - } + isPaused = true; + console.log('Scanner paused, dialogs:', openDialogCount); } // Scanner fortsetzen (wenn alle Dialoge geschlossen) @@ -385,29 +449,376 @@ if (!isScanning || !isPaused) return; - if (quaggaInitialized) { - try { - Quagga.start(); - isPaused = false; - console.log('Scanner resumed'); - startScanTimeout(); - } catch (e) { - console.log('Quagga resume error:', e); - } - } + isPaused = false; + console.log('Scanner resumed'); + startScanTimeout(); } // Globale Funktionen fuer PWA window._pauseScanner = pauseScanner; window._resumeScanner = resumeScanner; + // ===== OCR-Funktionen (Tesseract.js) ===== + + // Kamera-Preview starten (ohne Barcode-Detection) + function startCameraPreview() { + const video = elements.video; + + navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment' } + }) + .then(function(stream) { + video.srcObject = stream; + video.play(); + + isScanning = true; + scannerInitialized = true; + // OCR: Start-Button NICHT verstecken, sondern als "Foto aufnehmen" nutzen + elements.startBtn.classList.remove('hidden'); + elements.startBtn.textContent = 'Foto aufnehmen'; + elements.stopBtn.classList.remove('hidden'); + elements.videoContainer.classList.add('scanning'); + elements.startBtn.disabled = false; + + // Button-Click auf Snapshot-Capture umstellen + elements.startBtn.onclick = captureOCRSnapshot; + + console.log('OCR Kamera-Preview gestartet'); + }) + .catch(function(err) { + console.error('OCR Camera error:', err); + showToast('Kamerafehler: ' + err.message, 'error'); + elements.startBtn.disabled = false; + elements.startBtn.textContent = 'Scannen'; + }); + } + + // Foto vom Video-Stream aufnehmen und OCR durchführen + function captureOCRSnapshot() { + const video = elements.video; + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + // Canvas-Größe = Video-Größe + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + // Video-Frame auf Canvas zeichnen + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + // Canvas zu Blob konvertieren + canvas.toBlob(function(blob) { + if (blob) { + performOCR(blob); + } else { + showToast('Fehler beim Erstellen des Fotos', 'error'); + } + }, 'image/jpeg', 0.9); + } + + // Tesseract Worker initialisieren (Singleton, Lazy) + async function initOCRWorker() { + if (ocrInitialized && ocrWorker) { + return ocrWorker; + } + + try { + console.log('Initialisiere Tesseract Worker...'); + ocrWorker = await Tesseract.createWorker('eng'); + + // Nur Ziffern erkennen, einfache Einstellungen + await ocrWorker.setParameters({ + tessedit_char_whitelist: '0123456789' + // Keine PSM/OEM - Tesseract-Defaults verwenden + }); + + ocrInitialized = true; + console.log('Tesseract Worker bereit'); + return ocrWorker; + } catch (e) { + console.error('OCR Worker Init Error:', e); + showToast('OCR-Fehler: ' + e.message, 'error'); + throw e; + } + } + + // OCR durchführen mit Timeout + async function performOCR(imageBlob) { + // Loading-Overlay anzeigen + const overlay = document.createElement('div'); + overlay.className = 'ocr-loading-overlay'; + overlay.innerHTML = '
Erkenne Text...
'; + document.body.appendChild(overlay); + + try { + // Worker initialisieren (falls noch nicht geschehen) + const worker = await initOCRWorker(); + + // Bild vorverarbeiten für bessere Erkennung + const processedBlob = await preprocessImage(imageBlob); + + // OCR mit 10s Timeout + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('OCR Timeout')), 10000) + ); + + const ocrPromise = worker.recognize(processedBlob); + const result = await Promise.race([ocrPromise, timeoutPromise]); + + overlay.remove(); + + const recognizedText = result.data.text; + console.log('OCR Result:', recognizedText); + console.log('OCR Confidence:', result.data.confidence); + + // DEBUG: Erkannten Text anzeigen + showToast('OCR erkannte: "' + recognizedText.trim().substring(0, 50) + '"', 'info', 5000); + + // Ziffern extrahieren + const matches = extractDigitSequences(recognizedText, currentDigitCount); + console.log('Gefundene ' + currentDigitCount + '-stellige Zahlen:', matches); + + if (matches.length === 0) { + // Keine Treffer + showToast('Keine ' + currentDigitCount + '-stellige Zahl gefunden', 'error'); + playErrorBeep(); + } else if (matches.length === 1) { + // Ein Treffer - direkt verwenden + onOCRDetected(matches[0]); + } else { + // Mehrere Treffer - Auswahl anzeigen + showOCRSelectionModal(matches); + } + + } catch (e) { + overlay.remove(); + console.error('OCR Error:', e); + if (e.message === 'OCR Timeout') { + showToast('OCR-Timeout - bitte erneut versuchen', 'error'); + } else { + showToast('OCR-Fehler: ' + e.message, 'error'); + } + } + } + + // Bild vorverarbeiten: MINIMAL - nur Grayscale + async function preprocessImage(blob) { + return new Promise((resolve) => { + const img = new Image(); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + img.onload = function() { + // Original-Größe beibehalten - keine Skalierung + canvas.width = img.width; + canvas.height = img.height; + + // Bild zeichnen + ctx.drawImage(img, 0, 0); + + // NUR Grayscale - kein Kontrast, kein Binary! + // Tesseract macht das selbst besser + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + + for (let i = 0; i < data.length; i += 4) { + // Grayscale + const gray = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2]; + data[i] = data[i+1] = data[i+2] = gray; + } + + ctx.putImageData(imageData, 0, 0); + + // Zurück zu Blob + canvas.toBlob(function(processedBlob) { + resolve(processedBlob); + }, 'image/jpeg', 0.95); + }; + + img.src = URL.createObjectURL(blob); + }); + } + + // N-stellige Zahlensequenzen extrahieren - NUR zusammenhängende Zahlenblöcke mit EXAKT N Ziffern! + function extractDigitSequences(text, digitCount) { + // Alle zusammenhängenden Zahlenblöcke finden + const numberBlocks = text.match(/\d+/g) || []; + + // Nur Blöcke mit EXAKT digitCount Ziffern (keine Subsequenzen, keine Zusammensetzungen!) + const matches = numberBlocks.filter(function(block) { + return block.length === digitCount; + }); + + // Duplikate entfernen + return [...new Set(matches)]; + } + + // Auswahl-Modal bei mehreren OCR-Treffern + function showOCRSelectionModal(matches) { + pauseScanner(); + + const modal = document.createElement('div'); + modal.className = 'ocr-select-modal'; + modal.innerHTML = '
' + + '
' + + '

Mehrere Nummern gefunden

' + + '' + + '
' + + '
' + + matches.map(function(code) { + return '
' + code + '
'; + }).join('') + + '
' + + '
'; + + document.body.appendChild(modal); + + function closeModal() { + modal.remove(); + resumeScanner(); + } + + // Close button + modal.querySelector('.ocr-select-close').addEventListener('click', closeModal); + + // Item selection + modal.querySelectorAll('.ocr-select-item').forEach(function(item) { + item.addEventListener('click', function() { + const code = this.getAttribute('data-code'); + modal.remove(); + onOCRDetected(code); + resumeScanner(); + }); + }); + + // Backdrop click + modal.addEventListener('click', function(e) { + if (e.target === modal) closeModal(); + }); + } + + // OCR-Ergebnis verarbeiten (analog zu onBarcodeDetected) + function onOCRDetected(code) { + // Ignore detections wenn Scanner pausiert ist + if (isPaused) return; + + const format = 'OCR'; + + // Duplicate Check + if (lastScannedCode === code) { + showToast('Code bereits gescannt: ' + code, 'info'); + return; + } + + lastScannedCode = code; + console.log('OCR detected:', code); + + // Success Vibration + if ('vibrate' in navigator) { + navigator.vibrate([100, 50, 100]); + } + + // In Historie speichern + addToHistory(format, code); + + // Last Scan anzeigen + const formatLabels = { + 'OCR': 'OCR' + }; + const label = formatLabels[format] || format; + elements.lastScanCode.innerHTML = '' + label + '' + + '' + escapeHtml(code) + ''; + elements.lastScanArea.classList.remove('hidden'); + + // Toast + showToast('OCR: ' + code, 'success'); + + // Produktsuche starten + searchProduct(code); + } + + // Barcode-Typ wechseln (ohne Kamera-Neustart) + function switchBarcodeType(newType) { + if (newType === currentBarcodeType) return; + + console.log('Switching barcode type from', currentBarcodeType, 'to', newType); + + // OCR-Digit-Selector ein/ausblenden + const digitSelector = document.getElementById('ocr-digit-selector'); + if (digitSelector) { + digitSelector.classList.toggle('hidden', newType !== 'OCR'); + } + + // 1. Aktuellen Scanner stoppen (NICHT Kamera-Stream!) + if (currentScanner && isScanning) { + try { + currentScanner.reset(); + console.log('Scanner reset for type switch'); + // Stream bleibt aktiv! + } catch (e) { + console.log('Scanner reset error:', e); + } + } + + // 2. Format wechseln + currentBarcodeType = newType; + saveConfig('barcodeType', newType); + + // 3. Scanner mit neuem Format neu starten (falls er lief) + if (isScanning && newType !== 'OCR') { + // Barcode-Modus: ZXing neu starten, onclick zurücksetzen + elements.startBtn.onclick = null; + currentScanner = initZXing(newType); + } else if (isScanning && newType === 'OCR') { + // OCR-Modus: Stream läuft bereits, nur Button umstellen + elements.startBtn.classList.remove('hidden'); + elements.startBtn.textContent = 'Foto aufnehmen'; + elements.startBtn.disabled = false; + elements.startBtn.onclick = captureOCRSnapshot; + console.log('OCR-Modus aktiviert (Stream läuft bereits)'); + } + + // 4. UI-Feedback + const formatNames = { + 'CODE_128': 'Code 128', + 'QR_CODE': 'QR-Code', + 'DATA_MATRIX': 'DataMatrix', + 'OCR': 'OCR' + }; + showToast('Barcode-Typ: ' + (formatNames[newType] || newType), 'success'); + + // Scan-Region-Farbe andern + updateScanRegionColor(newType); + } + + // Scan-Region-Farbe je nach Barcode-Typ andern + function updateScanRegionColor(type) { + const colors = { + 'CODE_128': 'var(--primary)', // Blau (Standard) + 'QR_CODE': '#25a580', // Grun + 'DATA_MATRIX': '#bc9526', // Orange/Gelb + 'OCR': '#e74c3c' // Rot + }; + + const region = document.querySelector('.scan-region-highlight'); + if (region) { + region.style.borderColor = colors[type] || colors['CODE_128']; + } + } + function onBarcodeDetected(result) { - let code = result.codeResult.code; - const format = result.codeResult.format; + // Ignore detections wenn Scanner pausiert ist + if (isPaused) return; + + // ZXing Result-Format + let code = result.getText(); + let formatObj = result.getBarcodeFormat(); + // Format normalisieren: CODE_128, QR_CODE, DATA_MATRIX, EAN_13, EAN_8 + const format = formatObj ? formatObj.toString().toUpperCase() : 'UNKNOWN'; // EAN-13 detected but might be internal code with added check digit // If it's 13 digits and doesn't validate, try removing the last digit - if (format === 'ean_13' || (code.length === 13 && /^\d{13}$/.test(code))) { + if (format.includes('EAN') && format.includes('13') || (code.length === 13 && /^\d{13}$/.test(code))) { if (!validateEAN13(code)) { // Try without last digit (might be internal 12-digit code) const shortened = code.substring(0, 12); @@ -417,7 +828,7 @@ } // Validate EAN8 checksum - if (format === 'ean_8' && code.length === 8 && /^\d{8}$/.test(code)) { + if (format.includes('EAN') && format.includes('8') && code.length === 8 && /^\d{8}$/.test(code)) { if (!validateEAN8(code)) { // Try without last digit code = code.substring(0, 7); @@ -455,8 +866,27 @@ } playBeep(); + // Format-Label fuer Anzeige + const formatLabels = { + 'CODE_128': 'Code 128', + 'CODE_39': 'Code 39', + 'QR_CODE': 'QR', + 'DATA_MATRIX': 'DataMatrix', + 'EAN_13': 'EAN-13', + 'EAN_8': 'EAN-8' + }; + const label = formatLabels[format] || format; + showToast('Barcode: ' + code, 'success'); - elements.lastScanCode.textContent = code; + + // Last Scan mit Format-Badge anzeigen + elements.lastScanCode.innerHTML = ` + ${escapeHtml(label)} + ${escapeHtml(code)} + `; + + // In Historie speichern + addToHistory(format, code); // Search product searchProduct(code); @@ -467,6 +897,100 @@ }, 3000); } + // Barcode-Scan in Historie speichern (max. 10 Einträge) + function addToHistory(format, code) { + const cfg = loadConfig(); + let history = cfg.barcodeHistory || []; + + // Neuen Eintrag hinzufügen + history.unshift({ + type: format, + code: code, + timestamp: Math.floor(Date.now() / 1000) + }); + + // Maximal 10 Einträge behalten + if (history.length > 10) { + history = history.slice(0, 10); + } + + saveConfig('barcodeHistory', history); + } + + // Scan-Historie-Modal anzeigen + function showBarcodeHistory() { + pauseScanner(); + + const cfg = loadConfig(); + const history = cfg.barcodeHistory || []; + + if (history.length === 0) { + showToast('Noch keine Scans vorhanden', 'error'); + resumeScanner(); + return; + } + + const formatLabels = { + 'CODE_128': 'Code 128', + 'CODE_39': 'Code 39', + 'QR_CODE': 'QR', + 'DATA_MATRIX': 'DataMatrix', + 'EAN_13': 'EAN-13', + 'EAN_8': 'EAN-8' + }; + + const modal = document.createElement('div'); + modal.className = 'search-modal'; // Bestehende Modal-Styles wiederverwenden + modal.id = 'history-modal'; + modal.innerHTML = ` +
+
+

Scan-Historie (${history.length})

+ +
+
+ ${history.map((item, i) => { + const relativeTime = getRelativeTime(item.timestamp); + const label = formatLabels[item.type] || item.type; + return ` +
+
+ ${escapeHtml(label)} + ${escapeHtml(item.code)} +
+
+ ${relativeTime} +
+
+ `; + }).join('')} +
+
+ `; + document.body.appendChild(modal); + + function closeModal() { + modal.remove(); + resumeScanner(); + } + + document.getElementById('history-close').addEventListener('click', closeModal); + modal.addEventListener('click', function(e) { + if (e.target === modal) closeModal(); + }); + } + + // Relative Zeit-Anzeige (vor X Sekunden/Minuten/Stunden/Tagen) + function getRelativeTime(timestamp) { + const now = Math.floor(Date.now() / 1000); + const diff = now - timestamp; + + if (diff < 60) return 'vor ' + diff + ' Sek'; + if (diff < 3600) return 'vor ' + Math.floor(diff / 60) + ' Min'; + if (diff < 86400) return 'vor ' + Math.floor(diff / 3600) + ' Std'; + return 'vor ' + Math.floor(diff / 86400) + ' Tagen'; + } + // Validate EAN13 checksum function validateEAN13(code) { if (!/^\d{13}$/.test(code)) return false; diff --git a/pwa.php b/pwa.php index 1c1bc5e..2c99ac8 100755 --- a/pwa.php +++ b/pwa.php @@ -53,7 +53,7 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3'); Barcode Scanner - +