/**
* HandyBarcodeScanner - Mobile Barcode Scanner for Dolibarr
* Integrated version using SCANNER_CONFIG
*/
(function() {
'use strict';
// State
let CONFIG = null;
let currentMode = 'order';
let initialized = false;
// Global init function - can be called after login
window.initScanner = function() {
if (typeof window.SCANNER_CONFIG === 'undefined') {
console.log('HandyBarcodeScanner: Waiting for SCANNER_CONFIG...');
return false;
}
CONFIG = window.SCANNER_CONFIG;
// Validate required config
if (!CONFIG.ajaxUrl || !CONFIG.token) {
console.error('HandyBarcodeScanner: Invalid configuration');
return false;
}
currentMode = CONFIG.mode || 'order';
// Only init once
if (initialized) {
console.log('HandyBarcodeScanner: Already initialized');
return true;
}
init();
return true;
};
let isScanning = false;
let lastScannedCode = null;
let currentProduct = null;
let selectedSupplier = null;
let allSuppliers = [];
let quaggaInitialized = false;
// Multi-read confirmation - code must be read multiple times to be accepted
let pendingCode = null;
let pendingCodeCount = 0;
const REQUIRED_CONFIRMATIONS = 2; // Code must be read 2 times to be accepted
// Timeout: Hinweis wenn nichts erkannt wird
let scanTimeoutTimer = null;
const SCAN_TIMEOUT_MS = 8000; // 8 Sekunden ohne Erkennung
// 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;
}
// Mark as initialized
initialized = true;
console.log('HandyBarcodeScanner: Initialized');
// Check for camera support
checkCameraSupport();
bindEvents();
loadAllSuppliers();
// Globale Hooks fuer Tab-Switch (inline onclick)
window._scannerHideResult = hideResult;
window._scannerShowResult = showProductResult;
window._scannerCurrentProduct = null;
}
// Check camera support - just log warnings, never disable button
function checkCameraSupport() {
// Check HTTPS
const isSecure = location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1';
if (!isSecure) {
console.warn('HandyBarcodeScanner: HTTPS empfohlen fuer Kamerazugriff');
}
// Check if Quagga is loaded
if (typeof Quagga === 'undefined') {
console.error('HandyBarcodeScanner: Quagga nicht geladen');
}
// Check mediaDevices
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
console.warn('HandyBarcodeScanner: mediaDevices nicht gefunden');
}
// Never disable button - always allow click
}
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('Quagga Bibliothek nicht geladen', 'error');
return;
}
// Disable button while initializing
elements.startBtn.disabled = true;
elements.startBtn.textContent = 'Initialisiere...';
// First request camera permission explicitly
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
.then(function(stream) {
// Permission granted - stop the test stream
stream.getTracks().forEach(track => track.stop());
// Now start Quagga
initQuagga();
})
.catch(function(err) {
console.error('Camera permission error:', err);
elements.startBtn.disabled = false;
elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten';
let errorMsg = CONFIG.lang.cameraError || 'Kamerafehler';
if (err.name === 'NotAllowedError') {
errorMsg = 'Kamerazugriff verweigert. Bitte Berechtigung in den Browser-Einstellungen erteilen.';
} else if (err.name === 'NotFoundError') {
errorMsg = 'Keine Kamera gefunden.';
} else if (err.name === 'NotReadableError') {
errorMsg = 'Kamera wird bereits verwendet.';
}
showToast(errorMsg, 'error');
});
}
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%"
}
},
decoder: {
readers: [
"code_128_reader", // Best for internal codes - prioritize
"ean_reader",
"ean_8_reader",
"code_39_reader"
],
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);
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;
elements.startBtn.classList.add('hidden');
elements.stopBtn.classList.remove('hidden');
elements.videoContainer.classList.add('scanning');
// Register detection handler
Quagga.onDetected(onBarcodeDetected);
// Timeout-Timer starten
startScanTimeout();
});
}
// Timeout-Hinweis wenn nichts erkannt wird
function startScanTimeout() {
clearTimeout(scanTimeoutTimer);
scanTimeoutTimer = setTimeout(function() {
if (isScanning) {
showToast('Kein Barcode erkannt – naeher ran oder Barcode manuell eingeben', 'error');
// Timer erneut starten fuer wiederholten Hinweis
startScanTimeout();
}
}, SCAN_TIMEOUT_MS);
}
function stopScanner() {
if (!isScanning) return;
clearTimeout(scanTimeoutTimer);
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) {
let code = result.codeResult.code;
const format = result.codeResult.format;
// 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 (!validateEAN13(code)) {
// Try without last digit (might be internal 12-digit code)
const shortened = code.substring(0, 12);
console.log('HandyBarcodeScanner: Invalid EAN13, trying shortened:', shortened);
code = shortened;
}
}
// Validate EAN8 checksum
if (format === 'ean_8' && code.length === 8 && /^\d{8}$/.test(code)) {
if (!validateEAN8(code)) {
// Try without last digit
code = code.substring(0, 7);
}
}
// Already processed this code recently
if (code === lastScannedCode) return;
// Multi-read confirmation: same code must be detected multiple times
if (code === pendingCode) {
pendingCodeCount++;
} else {
// Different code - reset counter
pendingCode = code;
pendingCodeCount = 1;
}
// Not enough confirmations yet
if (pendingCodeCount < REQUIRED_CONFIRMATIONS) {
return;
}
// Code confirmed! Accept it
lastScannedCode = code;
pendingCode = null;
pendingCodeCount = 0;
// Timeout-Timer zuruecksetzen
startScanTimeout();
// Feedback
if (navigator.vibrate) {
navigator.vibrate([100, 50, 100]);
}
playBeep();
showToast('Barcode: ' + code, 'success');
elements.lastScanCode.textContent = code;
// Search product
searchProduct(code);
// Reset duplicate prevention after 3 seconds
setTimeout(() => {
lastScannedCode = null;
}, 3000);
}
// Validate EAN13 checksum
function validateEAN13(code) {
if (!/^\d{13}$/.test(code)) return false;
let sum = 0;
for (let i = 0; i < 12; i++) {
sum += parseInt(code[i]) * (i % 2 === 0 ? 1 : 3);
}
const checkDigit = (10 - (sum % 10)) % 10;
return checkDigit === parseInt(code[12]);
}
// Validate EAN8 checksum
function validateEAN8(code) {
if (!/^\d{8}$/.test(code)) return false;
let sum = 0;
for (let i = 0; i < 7; i++) {
sum += parseInt(code[i]) * (i % 2 === 0 ? 3 : 1);
}
const checkDigit = (10 - (sum % 10)) % 10;
return checkDigit === parseInt(code[7]);
}
// Shared AudioContext (must be created after user interaction)
let audioCtx = null;
function getAudioContext() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
// Resume if suspended (mobile browsers)
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
return audioCtx;
}
// Play success beep sound (high pitch)
function playBeep() {
try {
const ctx = getAudioContext();
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
oscillator.connect(gainNode);
gainNode.connect(ctx.destination);
oscillator.frequency.value = 1200; // Higher pitch for success
oscillator.type = 'sine';
gainNode.gain.value = 0.5;
oscillator.start();
oscillator.stop(ctx.currentTime + 0.15);
} catch (e) {
console.log('Audio not available:', e);
}
}
// Play error beep sound (low pitch, longer)
function playErrorBeep() {
try {
const ctx = getAudioContext();
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
oscillator.connect(gainNode);
gainNode.connect(ctx.destination);
oscillator.frequency.value = 300; // Low pitch for error
oscillator.type = 'square';
gainNode.gain.value = 0.4;
oscillator.start();
oscillator.stop(ctx.currentTime + 0.4);
} catch (e) {
console.log('Audio not available:', e);
}
}
// 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;
window._scannerCurrentProduct = data.product;
// Success feedback - double beep
playBeep();
setTimeout(playBeep, 150);
if (navigator.vibrate) navigator.vibrate([100, 100, 100]);
showToast('Produkt gefunden: ' + data.product.label, 'success');
showProductResult(data.product);
} else {
// Not found feedback - long vibration, error sound
if (navigator.vibrate) navigator.vibrate(500);
playErrorBeep();
showNotFound(barcode);
}
})
.catch(err => {
hideLoading();
console.error('Search error:', err);
if (navigator.vibrate) navigator.vibrate(500);
showToast(CONFIG.lang.error, 'error');
});
}
// Display Results based on Mode
function showProductResult(product) {
// Always read current mode from CONFIG (may have changed via tab click)
const mode = CONFIG.mode || currentMode;
switch (mode) {
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 = `