/** * HandyBarcodeScanner - Mobile Barcode Scanner for Dolibarr * Integrated version using SCANNER_CONFIG */ (function() { 'use strict'; // Use SCANNER_CONFIG from page (Dolibarr integrated) // Exit early if not on scanner page if (typeof window.SCANNER_CONFIG === 'undefined') { return; } const CONFIG = window.SCANNER_CONFIG; // Validate required config if (!CONFIG.ajaxUrl || !CONFIG.token) { console.error('HandyBarcodeScanner: Invalid configuration'); return; } // State let currentMode = CONFIG.mode || 'order'; let isScanning = false; let lastScannedCode = null; let currentProduct = null; let selectedSupplier = null; let allSuppliers = []; let quaggaInitialized = false; // DOM Elements (will be set on init) let elements = {}; // Initialize function init() { // Get DOM elements elements = { startBtn: document.getElementById('start-scan-btn'), stopBtn: document.getElementById('stop-scan-btn'), videoContainer: document.getElementById('scanner-video-container'), video: document.getElementById('scanner-video'), lastScanCode: document.getElementById('last-scan-code'), resultArea: document.getElementById('result-area'), manualInput: document.getElementById('manual-barcode-input'), manualSearchBtn: document.getElementById('manual-search-btn') }; if (!elements.startBtn || !elements.videoContainer) { // Not on scanner page - exit silently return; } // Check for camera support checkCameraSupport(); bindEvents(); loadAllSuppliers(); } // Check camera support and show appropriate messages function checkCameraSupport() { // Check HTTPS (required for camera access except on localhost) // Note: In test environments without HTTPS, camera may not work but we still allow trying const isSecure = location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1'; if (!isSecure) { // Show warning but don't disable - some browsers/configs may still work console.warn('HandyBarcodeScanner: HTTPS empfohlen für Kamerazugriff'); } // Check if getUserMedia is supported if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { showCameraError('Kamera wird von diesem Browser nicht unterstützt'); elements.startBtn.disabled = true; elements.startBtn.classList.add('butActionRefused'); elements.startBtn.classList.remove('butAction'); return; } // Check if Quagga is loaded if (typeof Quagga === 'undefined') { showCameraError('Scanner-Bibliothek nicht geladen'); elements.startBtn.disabled = true; elements.startBtn.classList.add('butActionRefused'); elements.startBtn.classList.remove('butAction'); return; } // All checks passed - enable button elements.startBtn.disabled = false; } function showCameraError(message) { const errorDiv = document.createElement('div'); errorDiv.className = 'error-message'; errorDiv.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #fff; text-align: center; padding: 20px;'; errorDiv.innerHTML = '
⚠️
' + message + '
'; elements.videoContainer.appendChild(errorDiv); } // Event Bindings function bindEvents() { elements.startBtn.addEventListener('click', startScanner); elements.stopBtn.addEventListener('click', stopScanner); // Manual barcode input if (elements.manualSearchBtn && elements.manualInput) { elements.manualSearchBtn.addEventListener('click', handleManualSearch); elements.manualInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { handleManualSearch(); } }); } } // Handle manual barcode input function handleManualSearch() { const barcode = elements.manualInput.value.trim(); if (!barcode) { showToast('Bitte Barcode eingeben', 'error'); return; } // Update last scan display elements.lastScanCode.textContent = barcode; // Vibration feedback if (CONFIG.enableVibration && navigator.vibrate) { navigator.vibrate(100); } // Search product searchProduct(barcode); // Clear input elements.manualInput.value = ''; } // Load all suppliers for manual selection function loadAllSuppliers() { fetch(CONFIG.ajaxUrl + 'getsuppliers.php?token=' + CONFIG.token) .then(res => res.json()) .then(data => { if (data.success) { allSuppliers = data.suppliers; } }) .catch(err => console.error('Error loading suppliers:', err)); } // Scanner Functions function startScanner() { if (isScanning) return; if (elements.startBtn.disabled) return; // Check if Quagga is available if (typeof Quagga === 'undefined') { showToast(CONFIG.lang.error + ': Scanner library not loaded', 'error'); return; } // Disable button while initializing elements.startBtn.disabled = true; elements.startBtn.textContent = 'Initialisiere...'; 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 } } }, decoder: { readers: [ "ean_reader", "ean_8_reader", "code_128_reader", "code_39_reader" ] }, locate: true, frequency: 10 }, function(err) { if (err) { console.error('Quagga init error:', err); elements.startBtn.disabled = false; elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten'; // Show specific error message let errorMsg = CONFIG.lang.cameraError || 'Kamerafehler'; if (err.name === 'NotAllowedError') { errorMsg = 'Kamerazugriff verweigert. Bitte Berechtigung erteilen.'; } else if (err.name === 'NotFoundError') { errorMsg = 'Keine Kamera gefunden.'; } else if (err.name === 'NotReadableError') { errorMsg = 'Kamera wird bereits verwendet.'; } showToast(errorMsg, 'error'); return; } // Success - start scanning Quagga.start(); quaggaInitialized = true; isScanning = true; elements.startBtn.classList.add('hidden'); elements.stopBtn.classList.remove('hidden'); elements.videoContainer.classList.add('scanning'); // Register detection handler Quagga.onDetected(onBarcodeDetected); }); } function stopScanner() { if (!isScanning) return; if (quaggaInitialized) { Quagga.offDetected(onBarcodeDetected); Quagga.stop(); } isScanning = false; quaggaInitialized = false; elements.startBtn.classList.remove('hidden'); elements.startBtn.disabled = false; elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten'; elements.stopBtn.classList.add('hidden'); elements.videoContainer.classList.remove('scanning'); } function onBarcodeDetected(result) { const code = result.codeResult.code; // Prevent duplicate scans if (code === lastScannedCode) return; lastScannedCode = code; // Vibration feedback if (CONFIG.enableVibration && navigator.vibrate) { navigator.vibrate(100); } // Sound feedback if (CONFIG.enableSound) { playBeep(); } elements.lastScanCode.textContent = code; // Search product searchProduct(code); // Reset duplicate prevention after 2 seconds setTimeout(() => { lastScannedCode = null; }, 2000); } // Play beep sound function playBeep() { try { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); oscillator.connect(gainNode); gainNode.connect(audioCtx.destination); oscillator.frequency.value = 1000; oscillator.type = 'sine'; gainNode.gain.value = 0.3; oscillator.start(); setTimeout(() => oscillator.stop(), 100); } catch (e) { console.log('Audio not available'); } } // Product Search function searchProduct(barcode) { showLoading(); fetch(CONFIG.ajaxUrl + 'findproduct.php?token=' + CONFIG.token + '&barcode=' + encodeURIComponent(barcode)) .then(res => res.json()) .then(data => { hideLoading(); if (data.success && data.product) { currentProduct = data.product; showProductResult(data.product); } else { showNotFound(barcode); } }) .catch(err => { hideLoading(); console.error('Search error:', err); showToast(CONFIG.lang.error, 'error'); }); } // Display Results based on Mode function showProductResult(product) { switch (currentMode) { case 'order': showOrderMode(product); break; case 'shop': showShopMode(product); break; case 'inventory': showInventoryMode(product); break; } elements.resultArea.classList.remove('hidden'); } // ORDER MODE function showOrderMode(product) { const suppliers = product.suppliers || []; const cheapest = suppliers.length > 0 ? suppliers[0] : null; let suppliersHtml = ''; if (suppliers.length > 0) { suppliersHtml = `
${CONFIG.lang.selectSupplier}: ${suppliers.map((s, i) => `
${escapeHtml(s.name)}${i === 0 ? '' + CONFIG.lang.cheapest + '' : ''}
${formatPrice(s.price)} €
`).join('')}
`; selectedSupplier = cheapest; } else { // No supplier assigned - show all available suppliers if (allSuppliers.length > 0) { suppliersHtml = `
${CONFIG.lang.noSupplier}
${CONFIG.lang.selectSupplier}: ${allSuppliers.map((s, i) => `
${escapeHtml(s.name)}
`).join('')}
`; selectedSupplier = allSuppliers[0]; } else { suppliersHtml = `
${CONFIG.lang.noSupplier}
`; } } elements.resultArea.innerHTML = `
${escapeHtml(product.label)}
${CONFIG.lang.ref}: ${escapeHtml(product.ref)}
${CONFIG.lang.stock}: ${product.stock} ${escapeHtml(product.stock_unit || 'Stk')}
${suppliersHtml}
${CONFIG.lang.quantity}:
`; bindOrderEvents(); } function bindOrderEvents() { // Supplier selection document.querySelectorAll('.supplier-option').forEach(opt => { opt.addEventListener('click', function() { document.querySelectorAll('.supplier-option').forEach(o => o.classList.remove('selected')); this.classList.add('selected'); selectedSupplier = { id: this.dataset.supplierId, price: parseFloat(this.dataset.price) || 0 }; }); }); // Quantity controls const qtyInput = document.getElementById('qty-input'); document.getElementById('qty-minus').addEventListener('click', () => { const val = parseInt(qtyInput.value) || 1; if (val > 1) qtyInput.value = val - 1; }); document.getElementById('qty-plus').addEventListener('click', () => { const val = parseInt(qtyInput.value) || 1; qtyInput.value = val + 1; }); // Add to order document.getElementById('add-to-order').addEventListener('click', addToOrder); } function addToOrder() { if (!currentProduct || !selectedSupplier) { showToast(CONFIG.lang.error, 'error'); return; } const qty = parseInt(document.getElementById('qty-input').value) || 1; showLoading(); fetch(CONFIG.ajaxUrl + 'addtoorder.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `token=${CONFIG.token}&product_id=${currentProduct.id}&supplier_id=${selectedSupplier.id}&qty=${qty}&price=${selectedSupplier.price || 0}` }) .then(res => res.json()) .then(data => { hideLoading(); if (data.success) { showToast(`${CONFIG.lang.added}: ${currentProduct.label} (${qty}x)`, 'success'); hideResult(); } else { showToast(data.error || CONFIG.lang.error, 'error'); } }) .catch(err => { hideLoading(); console.error('Add to order error:', err); showToast(CONFIG.lang.error, 'error'); }); } // SHOP MODE function showShopMode(product) { const suppliers = product.suppliers || []; let linksHtml = ''; if (suppliers.length > 0) { linksHtml = suppliers.map(s => { if (s.url) { return ` ${escapeHtml(s.name)} `; } return ` `; }).join(''); } else { linksHtml = `
${CONFIG.lang.noSupplier}
`; } elements.resultArea.innerHTML = `
${escapeHtml(product.label)}
${CONFIG.lang.ref}: ${escapeHtml(product.ref)}
${CONFIG.lang.stock}: ${product.stock} ${escapeHtml(product.stock_unit || 'Stk')}
`; } // INVENTORY MODE function showInventoryMode(product) { elements.resultArea.innerHTML = `
${escapeHtml(product.label)}
${CONFIG.lang.ref}: ${escapeHtml(product.ref)}
${product.stock}
${CONFIG.lang.currentStock}
${product.stock}
${CONFIG.lang.newStock}
${CONFIG.lang.newStock}:
`; bindInventoryEvents(product.stock); } function bindInventoryEvents(originalStock) { const stockInput = document.getElementById('stock-input'); const preview = document.getElementById('new-stock-preview'); function updatePreview() { preview.textContent = stockInput.value; } stockInput.addEventListener('input', updatePreview); document.getElementById('stock-minus').addEventListener('click', () => { const val = parseInt(stockInput.value) || 0; if (val > 0) { stockInput.value = val - 1; updatePreview(); } }); document.getElementById('stock-plus').addEventListener('click', () => { const val = parseInt(stockInput.value) || 0; stockInput.value = val + 1; updatePreview(); }); document.getElementById('save-stock').addEventListener('click', () => { const newStock = parseInt(stockInput.value) || 0; if (newStock !== originalStock) { showConfirmDialog(originalStock, newStock); } else { showToast(CONFIG.lang.saved, 'success'); hideResult(); } }); } function showConfirmDialog(oldStock, newStock) { const dialog = document.createElement('div'); dialog.className = 'confirm-dialog'; dialog.innerHTML = `
${CONFIG.lang.confirmStockChange}
${oldStock} → ${newStock}
`; document.body.appendChild(dialog); document.getElementById('confirm-cancel').addEventListener('click', () => { dialog.remove(); }); document.getElementById('confirm-ok').addEventListener('click', () => { dialog.remove(); saveStock(newStock); }); } function saveStock(newStock) { if (!currentProduct) return; showLoading(); fetch(CONFIG.ajaxUrl + 'updatestock.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `token=${CONFIG.token}&product_id=${currentProduct.id}&stock=${newStock}` }) .then(res => res.json()) .then(data => { hideLoading(); if (data.success) { showToast(CONFIG.lang.saved, 'success'); hideResult(); } else { showToast(data.error || CONFIG.lang.error, 'error'); } }) .catch(err => { hideLoading(); console.error('Save stock error:', err); showToast(CONFIG.lang.error, 'error'); }); } // Not Found function showNotFound(barcode) { elements.resultArea.innerHTML = `
${CONFIG.lang.productNotFound}
${escapeHtml(barcode)}
`; elements.resultArea.classList.remove('hidden'); } // UI Helpers function hideResult() { elements.resultArea.classList.add('hidden'); elements.resultArea.innerHTML = ''; currentProduct = null; selectedSupplier = null; } function showLoading() { // Use jQuery blockUI if available (Dolibarr standard) if (typeof jQuery !== 'undefined' && jQuery.blockUI) { jQuery.blockUI({ message: '
' }); } } function hideLoading() { if (typeof jQuery !== 'undefined' && jQuery.unblockUI) { jQuery.unblockUI(); } } function showToast(message, type = 'success') { // Remove existing toasts document.querySelectorAll('.scanner-toast').forEach(t => t.remove()); const toast = document.createElement('div'); toast.className = 'scanner-toast ' + type; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); } function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function formatPrice(price) { return parseFloat(price).toFixed(2).replace('.', ','); } // Start when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();