/**
* 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 isPaused = false; // Scanner pausiert (Dialog offen)
let lastScannedCode = null;
let currentProduct = null;
let selectedSupplier = null;
let allSuppliers = [];
let quaggaInitialized = false;
let openDialogCount = 0; // Zaehlt offene Dialoge
// Persistente Einstellungen - eine zentrale Config statt einzelner Keys
const STORAGE_KEY = 'hbs_config';
function loadConfig() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
} catch (e) {
return {};
}
}
function saveConfig(key, value) {
const cfg = loadConfig();
cfg[key] = value;
localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg));
}
// Migration: alte einzelne Keys -> neue zentrale Config
(function migrateOldKeys() {
const oldSupplier = localStorage.getItem('hbs_lastFreetextSupplierId');
const oldOrder = localStorage.getItem('hbs_lastOrderId');
if (oldSupplier) {
saveConfig('lastFreetextSupplierId', oldSupplier);
localStorage.removeItem('hbs_lastFreetextSupplierId');
}
if (oldOrder) {
saveConfig('lastOrderId', parseInt(oldOrder));
localStorage.removeItem('hbs_lastOrderId');
}
})();
let lastFreetextSupplierId = loadConfig().lastFreetextSupplierId || null;
// Order overview state (persistent über App-Neustarts)
let lastOrderId = loadConfig().lastOrderId || null;
let cachedOrders = [];
let currentOrderDetail = null;
// 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;
// Globale Hooks fuer Toolbar-Buttons (PWA)
window._showProductSearchModal = showProductSearchModal;
window._showFreetextModal = showFreetextModal;
window._showOrdersPanel = showOrdersPanel;
}
// 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, {credentials: 'same-origin'})
.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 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);
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.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();
});
}
// 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() {
clearTimeout(scanTimeoutTimer);
if (quaggaInitialized) {
try {
Quagga.offDetected(onBarcodeDetected);
Quagga.stop();
} catch (e) {
console.log('Quagga stop error:', e);
}
}
isScanning = false;
isPaused = false;
quaggaInitialized = false;
elements.startBtn.classList.remove('hidden');
elements.startBtn.disabled = false;
elements.startBtn.textContent = CONFIG.lang.startScan || 'Scannen';
elements.stopBtn.classList.add('hidden');
elements.videoContainer.classList.remove('scanning');
}
// Scanner pausieren (bei Dialog-Oeffnung)
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);
}
}
}
// Scanner fortsetzen (wenn alle Dialoge geschlossen)
function resumeScanner() {
openDialogCount = Math.max(0, openDialogCount - 1);
if (openDialogCount > 0) {
console.log('Still dialogs open:', openDialogCount);
return;
}
if (!isScanning || !isPaused) return;
if (quaggaInitialized) {
try {
Quagga.start();
isPaused = false;
console.log('Scanner resumed');
startScanTimeout();
} catch (e) {
console.log('Quagga resume error:', e);
}
}
}
// Globale Funktionen fuer PWA
window._pauseScanner = pauseScanner;
window._resumeScanner = resumeScanner;
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), {credentials: 'same-origin'})
.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 = `
${CONFIG.lang.selectSupplier}:
${suppliers.map((s, i) => `
${escapeHtml(s.name)}${i === 0 ? '' + CONFIG.lang.cheapest + '' : ''}
${s.ref_fourn ? '
' + CONFIG.lang.supplierOrderRef + ': ' + escapeHtml(s.ref_fourn) + '
' : ''}
${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) => `
`).join('')}
`;
selectedSupplier = allSuppliers[0];
} else {
suppliersHtml = `${CONFIG.lang.noSupplier}
`;
}
}
elements.resultArea.innerHTML = `
←
${suppliersHtml}
`;
bindOrderEvents();
bindOrderToolsEvents();
bindSwipeEvents();
}
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);
// Print barcode button
const printBtn = document.getElementById('btn-print-barcode');
if (printBtn) {
printBtn.addEventListener('click', function() {
if (typeof window._showPrintBarcodeModal === 'function') {
window._showPrintBarcodeModal(currentProduct);
}
});
}
}
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',
credentials: 'same-origin',
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) {
const productName = currentProduct.label || currentProduct.ref || 'Produkt';
showToast(`${CONFIG.lang.added}: ${productName} (${qty}x)`, 'success');
// Save last order ID for auto-open in order view
lastOrderId = data.order_id;
saveConfig('lastOrderId', data.order_id);
hideResult();
} else {
showToast(data.error || CONFIG.lang.error, 'error');
}
})
.catch(err => {
hideLoading();
console.error('Add to order error:', err);
showToast(CONFIG.lang.error, 'error');
});
}
// Bind order tools (search + freetext buttons)
function bindOrderToolsEvents() {
const searchBtn = document.getElementById('btn-product-search');
const freetextBtn = document.getElementById('btn-freetext');
if (searchBtn) {
searchBtn.addEventListener('click', showProductSearchModal);
}
if (freetextBtn) {
freetextBtn.addEventListener('click', showFreetextModal);
}
}
// PRODUCT SEARCH MODAL
function showProductSearchModal() {
pauseScanner();
const modal = document.createElement('div');
modal.className = 'search-modal';
modal.id = 'search-modal';
modal.innerHTML = `
`;
document.body.appendChild(modal);
const input = document.getElementById('product-search-input');
const results = document.getElementById('search-results');
let searchTimeout = null;
function closeModal() {
modal.remove();
resumeScanner();
}
input.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (query.length < 2) {
results.innerHTML = '';
return;
}
results.innerHTML = 'Suche...
';
searchTimeout = setTimeout(() => {
searchProducts(query, closeModal);
}, 300);
});
document.getElementById('search-close').addEventListener('click', closeModal);
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeModal();
}
});
input.focus();
}
function searchProducts(query, closeModalCallback) {
fetch(CONFIG.ajaxUrl + 'searchproduct.php?token=' + CONFIG.token + '&q=' + encodeURIComponent(query), {credentials: 'same-origin'})
.then(res => res.json())
.then(data => {
const results = document.getElementById('search-results');
if (!results) return;
if (data.success && data.products.length > 0) {
results.innerHTML = data.products.map(p => `
${escapeHtml(p.label)}
Ref: ${escapeHtml(p.ref)}
Lager: ${p.stock} ${escapeHtml(p.stock_unit || 'Stk')}
`).join('');
results.querySelectorAll('.search-result-item').forEach(item => {
item.addEventListener('click', function() {
const product = JSON.parse(this.dataset.product);
if (closeModalCallback) closeModalCallback();
currentProduct = product;
window._scannerCurrentProduct = product;
showProductResult(product);
});
});
} else {
results.innerHTML = 'Keine Produkte gefunden
';
}
})
.catch(err => {
console.error('Search error:', err);
const results = document.getElementById('search-results');
if (results) {
results.innerHTML = 'Fehler bei der Suche
';
}
});
}
// FREETEXT MODAL
function showFreetextModal() {
// Need a supplier for freetext
if (allSuppliers.length === 0) {
showToast('Keine Lieferanten verfügbar', 'error');
return;
}
pauseScanner();
const savedSupplierId = loadConfig().lastFreetextSupplierId;
const modal = document.createElement('div');
modal.className = 'freetext-modal';
modal.id = 'freetext-modal';
modal.innerHTML = `
Freitext-Position
`;
document.body.appendChild(modal);
// Gespeicherten Lieferant per JS setzen
const supplierSelect = document.getElementById('freetext-supplier');
if (savedSupplierId) {
supplierSelect.value = savedSupplierId;
}
// Bei jeder Aenderung sofort speichern
supplierSelect.addEventListener('change', function() {
saveConfig('lastFreetextSupplierId', this.value);
});
function closeModal() {
modal.remove();
resumeScanner();
}
document.getElementById('freetext-cancel').addEventListener('click', closeModal);
document.getElementById('freetext-add').addEventListener('click', function() {
addFreetextLine(closeModal);
});
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeModal();
}
});
document.getElementById('freetext-desc').focus();
}
function addFreetextLine(closeModalCallback) {
const supplierId = document.getElementById('freetext-supplier').value;
const description = document.getElementById('freetext-desc').value.trim();
const qty = parseFloat(document.getElementById('freetext-qty').value) || 1;
const price = parseFloat(document.getElementById('freetext-price').value) || 0;
if (!description) {
showToast('Bitte Beschreibung eingeben', 'error');
return;
}
// Lieferant merken für nächstes Mal (auch über Neustarts hinweg)
lastFreetextSupplierId = supplierId;
saveConfig('lastFreetextSupplierId', supplierId);
showLoading();
fetch(CONFIG.ajaxUrl + 'addfreetextline.php', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `token=${CONFIG.token}&supplier_id=${supplierId}&description=${encodeURIComponent(description)}&qty=${qty}&price=${price}`
})
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
showToast(`Hinzugefügt: ${description} (${qty}x)`, 'success');
lastOrderId = data.order_id;
saveConfig('lastOrderId', data.order_id);
if (closeModalCallback) closeModalCallback();
} else {
showToast(data.error || CONFIG.lang.error, 'error');
}
})
.catch(err => {
hideLoading();
console.error('Add freetext error:', err);
showToast(CONFIG.lang.error, 'error');
});
}
// ORDERS PANEL AS MODAL (for toolbar button)
function showOrdersPanel() {
pauseScanner();
const modal = document.createElement('div');
modal.className = 'orders-modal';
modal.id = 'orders-modal';
modal.innerHTML = `
`;
document.body.appendChild(modal);
function closeModal() {
modal.remove();
resumeScanner();
}
// Close button
document.getElementById('orders-modal-close').addEventListener('click', closeModal);
// Click outside to close
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeModal();
}
});
// Back button in detail view
document.getElementById('orders-modal-detail-back').addEventListener('click', function() {
document.getElementById('orders-modal-detail').classList.add('hidden');
document.getElementById('orders-modal-scroller').classList.remove('hidden');
});
// Load orders
loadOrdersIntoModal();
}
function loadOrdersIntoModal() {
const scroller = document.getElementById('orders-modal-scroller');
if (!scroller) return;
scroller.innerHTML = '';
fetch(CONFIG.ajaxUrl + 'getorders.php?token=' + CONFIG.token, {credentials: 'same-origin'})
.then(res => res.json())
.then(data => {
if (data.success) {
cachedOrders = data.orders;
renderOrdersInModal(data.orders);
} else {
scroller.innerHTML = 'Fehler beim Laden
';
}
})
.catch(err => {
console.error('Load orders error:', err);
scroller.innerHTML = 'Fehler beim Laden
';
});
}
function renderOrdersInModal(orders) {
const scroller = document.getElementById('orders-modal-scroller');
if (!scroller) return;
if (orders.length === 0) {
scroller.innerHTML = 'Keine Bestellungen
';
return;
}
scroller.innerHTML = orders.map(order => {
const statusClass = order.status === 0 ? 'draft' : 'validated';
const isActive = order.id === lastOrderId;
const direktClass = order.is_direkt ? 'direkt' : '';
const canDelete = order.status === 0; // Nur Entwürfe können gelöscht werden
return `
${canDelete ? `
` : ''}
${escapeHtml(order.supplier_name)}
${escapeHtml(order.ref)}
${escapeHtml(order.status_label)}
${order.line_count} Pos. / ${order.total_qty} Stk
`;
}).join('');
// Bind click events for order cards
scroller.querySelectorAll('.order-card').forEach(card => {
card.addEventListener('click', function(e) {
// Ignoriere Klicks auf den Lösch-Button
if (e.target.closest('.order-delete-btn')) return;
const orderId = parseInt(this.dataset.orderId);
openOrderDetailInModal(orderId);
});
});
// Bind delete button events
scroller.querySelectorAll('.order-delete-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const orderId = parseInt(this.dataset.orderId);
const order = cachedOrders.find(o => o.id === orderId);
showDeleteOrderDialog(orderId, order ? order.ref : '');
});
});
}
function showDeleteOrderDialog(orderId, orderRef) {
const dialog = document.createElement('div');
dialog.className = 'line-edit-dialog';
dialog.id = 'delete-order-dialog';
dialog.innerHTML = `
Bestellung löschen?
${escapeHtml(orderRef)}
Diese Aktion kann nicht rückgängig gemacht werden.
`;
document.body.appendChild(dialog);
function closeDialog() {
dialog.remove();
}
document.getElementById('delete-order-cancel').addEventListener('click', closeDialog);
document.getElementById('delete-order-confirm').addEventListener('click', function() {
closeDialog();
deleteOrder(orderId);
});
dialog.addEventListener('click', function(e) {
if (e.target === dialog) closeDialog();
});
}
function deleteOrder(orderId) {
showLoading();
fetch(CONFIG.ajaxUrl + 'deleteorder.php', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `token=${CONFIG.token}&order_id=${orderId}`
})
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
showToast('Bestellung gelöscht', 'success');
loadOrdersIntoModal();
} else {
showToast(data.error || CONFIG.lang.error, 'error');
}
})
.catch(err => {
hideLoading();
console.error('Delete order error:', err);
showToast(CONFIG.lang.error, 'error');
});
}
function openOrderDetailInModal(orderId) {
const scroller = document.getElementById('orders-modal-scroller');
const detailSection = document.getElementById('orders-modal-detail');
const content = document.getElementById('orders-modal-detail-content');
const title = document.getElementById('orders-modal-detail-title');
if (!scroller || !detailSection || !content) return;
scroller.classList.add('hidden');
detailSection.classList.remove('hidden');
content.innerHTML = '';
fetch(CONFIG.ajaxUrl + 'getorderlines.php?token=' + CONFIG.token + '&order_id=' + orderId, {credentials: 'same-origin'})
.then(res => res.json())
.then(data => {
if (data.success) {
currentOrderDetail = data.order;
title.textContent = data.order.ref + ' - ' + data.order.supplier_name;
renderOrderLinesInModal(data.lines, data.order, orderId);
} else {
content.innerHTML = 'Fehler beim Laden
';
}
})
.catch(err => {
console.error('Load order lines error:', err);
content.innerHTML = 'Fehler beim Laden
';
});
}
// Cache für aktuelle Bestellzeilen (Modal)
let currentModalLines = [];
function renderOrderLinesInModal(lines, order, orderId) {
const content = document.getElementById('orders-modal-detail-content');
if (!content) return;
// Lines cachen für Click-Handler
currentModalLines = lines;
if (lines.length === 0) {
content.innerHTML = 'Keine Positionen
';
return;
}
const canEdit = order.status == 0;
content.innerHTML = lines.map(line => `
${escapeHtml(line.product_label)}
${line.product_ref ? 'Ref: ' + escapeHtml(line.product_ref) : ''} ${line.is_freetext ? '(Freitext)' : (line.stock > 0 ? '| Lager: ' + line.stock : '')}
${line.qty}x
${canEdit ? `
` : ''}
`).join('');
// Edit-Button Events
content.querySelectorAll('.order-line-edit-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const lineId = parseInt(this.dataset.lineId);
const line = currentModalLines.find(l => l.id === lineId);
if (line) {
showLineEditDialogInModal(line, orderId);
}
});
});
}
function showLineEditDialogInModal(line, orderId) {
pauseScanner();
const isFreetext = line.is_freetext || !line.product_id;
const dialog = document.createElement('div');
dialog.className = 'line-edit-dialog';
dialog.id = 'line-edit-dialog';
dialog.innerHTML = `
${isFreetext ? 'Freitext-Position' : escapeHtml(line.product_label)}
${isFreetext ? `
` : `
Lagerbestand: ${line.stock}
`}
`;
document.body.appendChild(dialog);
function closeDialog() {
dialog.remove();
resumeScanner();
}
document.getElementById('line-save').addEventListener('click', () => {
const newQty = parseFloat(document.getElementById('line-qty-input').value) || 1;
const descInput = document.getElementById('line-desc-input');
const newDesc = descInput ? descInput.value.trim() : null;
updateOrderLineInModal(orderId, line.id, 'update', newQty, newDesc);
closeDialog();
});
document.getElementById('line-delete').addEventListener('click', () => {
if (confirm('Position wirklich löschen?')) {
updateOrderLineInModal(orderId, line.id, 'delete');
closeDialog();
}
});
dialog.addEventListener('click', function(e) {
if (e.target === dialog) {
closeDialog();
}
});
}
function updateOrderLineInModal(orderId, lineId, action, qty = 0, description = null) {
showLoading();
let body = `token=${CONFIG.token}&order_id=${orderId}&line_id=${lineId}&action=${action}`;
if (action === 'update') {
body += `&qty=${qty}`;
if (description !== null) {
body += `&description=${encodeURIComponent(description)}`;
}
}
fetch(CONFIG.ajaxUrl + 'updateorderline.php', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: body
})
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
showToast(action === 'delete' ? 'Position gelöscht' : 'Anzahl aktualisiert', 'success');
if (data.order_deleted) {
// Go back to orders list
document.getElementById('orders-modal-detail').classList.add('hidden');
document.getElementById('orders-modal-scroller').classList.remove('hidden');
loadOrdersIntoModal();
} else {
// Reload order detail
openOrderDetailInModal(orderId);
}
} else {
showToast(data.error || CONFIG.lang.error, 'error');
}
})
.catch(err => {
hideLoading();
console.error('Update line error:', err);
showToast(CONFIG.lang.error, 'error');
});
}
// SWIPE GESTURE FOR ORDER OVERVIEW
function bindSwipeEvents() {
const container = document.getElementById('order-view-container');
if (!container) return;
let startX = 0;
let startY = 0;
let isDragging = false;
container.addEventListener('touchstart', function(e) {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
isDragging = true;
}, { passive: true });
container.addEventListener('touchmove', function(e) {
if (!isDragging) return;
const deltaX = e.touches[0].clientX - startX;
const deltaY = e.touches[0].clientY - startY;
// Only horizontal swipe
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 30) {
e.preventDefault();
}
}, { passive: false });
container.addEventListener('touchend', function(e) {
if (!isDragging) return;
isDragging = false;
const endX = e.changedTouches[0].clientX;
const deltaX = endX - startX;
// Swipe left to show orders
if (deltaX < -50 && !container.classList.contains('swiped')) {
container.classList.add('swiped');
loadOrders();
}
// Swipe right to go back
else if (deltaX > 50) {
if (container.classList.contains('detail-open')) {
container.classList.remove('detail-open');
} else if (container.classList.contains('swiped')) {
container.classList.remove('swiped');
}
}
}, { passive: true });
// Back buttons
const ordersBack = document.getElementById('orders-back');
const detailBack = document.getElementById('order-detail-back');
if (ordersBack) {
ordersBack.addEventListener('click', function() {
container.classList.remove('swiped');
});
}
if (detailBack) {
detailBack.addEventListener('click', function() {
container.classList.remove('detail-open');
});
}
}
// LOAD ORDERS
function loadOrders() {
const scroller = document.getElementById('orders-scroller');
if (!scroller) return;
scroller.innerHTML = '';
fetch(CONFIG.ajaxUrl + 'getorders.php?token=' + CONFIG.token, {credentials: 'same-origin'})
.then(res => res.json())
.then(data => {
if (data.success) {
cachedOrders = data.orders;
renderOrders(data.orders);
} else {
scroller.innerHTML = 'Fehler beim Laden
';
}
})
.catch(err => {
console.error('Load orders error:', err);
scroller.innerHTML = 'Fehler beim Laden
';
});
}
function renderOrders(orders) {
const scroller = document.getElementById('orders-scroller');
if (!scroller) return;
if (orders.length === 0) {
scroller.innerHTML = 'Keine Bestellungen
';
return;
}
scroller.innerHTML = orders.map(order => {
const statusClass = order.status === 0 ? 'draft' : 'validated';
const isActive = order.id === lastOrderId;
const direktClass = order.is_direkt ? 'direkt' : '';
return `
${escapeHtml(order.supplier_name)}
${escapeHtml(order.ref)}
${escapeHtml(order.status_label)}
${order.line_count} Pos. / ${order.total_qty} Stk
`;
}).join('');
// Bind click events
scroller.querySelectorAll('.order-card').forEach(card => {
card.addEventListener('click', function() {
const orderId = parseInt(this.dataset.orderId);
openOrderDetail(orderId);
});
});
// Auto-scroll to active order
if (lastOrderId) {
const activeCard = scroller.querySelector('.order-card.active');
if (activeCard) {
activeCard.scrollIntoView({ behavior: 'smooth', inline: 'center' });
}
}
}
// OPEN ORDER DETAIL
function openOrderDetail(orderId) {
const container = document.getElementById('order-view-container');
const content = document.getElementById('order-detail-content');
const title = document.getElementById('order-detail-title');
if (!container || !content) return;
content.innerHTML = '';
container.classList.add('detail-open');
fetch(CONFIG.ajaxUrl + 'getorderlines.php?token=' + CONFIG.token + '&order_id=' + orderId, {credentials: 'same-origin'})
.then(res => res.json())
.then(data => {
if (data.success) {
currentOrderDetail = data.order;
title.textContent = data.order.ref + ' - ' + data.order.supplier_name;
renderOrderLines(data.lines, data.order);
} else {
content.innerHTML = 'Fehler beim Laden
';
}
})
.catch(err => {
console.error('Load order lines error:', err);
content.innerHTML = 'Fehler beim Laden
';
});
}
function renderOrderLines(lines, order) {
const content = document.getElementById('order-detail-content');
if (!content) return;
if (lines.length === 0) {
content.innerHTML = 'Keine Positionen
';
return;
}
const canEdit = order.status == 0; // Only draft orders can be edited
content.innerHTML = lines.map(line => `
${escapeHtml(line.product_label)}
${line.product_ref ? 'Ref: ' + escapeHtml(line.product_ref) : ''} ${line.stock > 0 ? '| Lager: ' + line.stock : ''}
${line.qty}x
`).join('');
if (canEdit) {
content.querySelectorAll('.order-line').forEach(lineEl => {
lineEl.addEventListener('click', function() {
const lineId = parseInt(this.dataset.lineId);
const orderId = parseInt(this.dataset.orderId);
const line = lines.find(l => l.id === lineId);
if (line) {
showLineEditDialog(line, orderId);
}
});
});
}
}
// LINE EDIT DIALOG
function showLineEditDialog(line, orderId) {
pauseScanner();
const isFreetext = line.is_freetext || !line.product_id;
const dialog = document.createElement('div');
dialog.className = 'line-edit-dialog';
dialog.id = 'line-edit-dialog';
dialog.innerHTML = `
${isFreetext ? 'Freitext-Position' : escapeHtml(line.product_label)}
${isFreetext ? `
` : `
Lagerbestand: ${line.stock}
`}
`;
document.body.appendChild(dialog);
function closeDialog() {
dialog.remove();
resumeScanner();
}
document.getElementById('line-save').addEventListener('click', () => {
const newQty = parseFloat(document.getElementById('line-qty-input').value) || 1;
const descInput = document.getElementById('line-desc-input');
const newDesc = descInput ? descInput.value.trim() : null;
updateOrderLine(orderId, line.id, 'update', newQty, newDesc);
closeDialog();
});
document.getElementById('line-delete').addEventListener('click', () => {
if (confirm('Position wirklich löschen?')) {
updateOrderLine(orderId, line.id, 'delete');
closeDialog();
}
});
dialog.addEventListener('click', function(e) {
if (e.target === dialog) {
closeDialog();
}
});
}
function updateOrderLine(orderId, lineId, action, qty = 0, description = null) {
showLoading();
let body = `token=${CONFIG.token}&order_id=${orderId}&line_id=${lineId}&action=${action}`;
if (action === 'update') {
body += `&qty=${qty}`;
if (description !== null) {
body += `&description=${encodeURIComponent(description)}`;
}
}
fetch(CONFIG.ajaxUrl + 'updateorderline.php', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: body
})
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
showToast(action === 'delete' ? 'Position gelöscht' : 'Anzahl aktualisiert', 'success');
if (data.order_deleted) {
// Go back to orders list
const container = document.getElementById('order-view-container');
if (container) {
container.classList.remove('detail-open');
}
loadOrders();
} else {
// Reload order detail
openOrderDetail(orderId);
}
} else {
showToast(data.error || CONFIG.lang.error, 'error');
}
})
.catch(err => {
hideLoading();
console.error('Update line 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 => {
// shop_url + artikelnummer
let shopUrl = s.url || '';
if (shopUrl && s.ref_fourn) {
shopUrl = shopUrl + s.ref_fourn;
}
if (shopUrl) {
return `
${escapeHtml(s.name)}
${s.ref_fourn ? '' + escapeHtml(s.ref_fourn) + '' : ''}
→
`;
}
return `
${escapeHtml(s.name)}
—
`;
}).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')}
${CONFIG.lang.openShop}:
${linksHtml}
`;
}
// 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}
`;
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) {
pauseScanner();
const dialog = document.createElement('div');
dialog.className = 'confirm-dialog';
dialog.innerHTML = `
${CONFIG.lang.confirmStockChange}
${oldStock} → ${newStock}
`;
document.body.appendChild(dialog);
function closeDialog() {
dialog.remove();
resumeScanner();
}
document.getElementById('confirm-cancel').addEventListener('click', closeDialog);
document.getElementById('confirm-ok').addEventListener('click', () => {
closeDialog();
saveStock(newStock);
});
}
function saveStock(newStock) {
if (!currentProduct) return;
showLoading();
fetch(CONFIG.ajaxUrl + 'updatestock.php', {
method: 'POST',
credentials: 'same-origin',
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;
window._scannerCurrentProduct = 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 (only if CONFIG already exists)
function tryInit() {
if (typeof window.SCANNER_CONFIG !== 'undefined') {
window.initScanner();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', tryInit);
} else {
tryInit();
}
})();