dolibarr.handybarcodescanner/js/scanner.js
data 74728a71d1 v5.0: Bestellmodus erweitert, Barcode-Druck, Scanner-Pausierung
Bestellmodus:
- Produktsuche (Lupe-Button) und Freitext-Positionen (Plus-Button)
- Bestellübersicht per Swipe nach links
- Horizontale Bestellliste mit Direkt-Bestellungen hervorgehoben
- Bestellzeilen bearbeiten (Menge ändern, löschen)
- Auto-Öffnung der zuletzt bearbeiteten Bestellung

Barcode-Druck:
- Swipe nach rechts öffnet Produktsuche für Druck
- Code128 Barcode-Generierung mit JsBarcode
- Optimiert für 24mm Etikettendrucker (Brother P-touch)

Scanner:
- Automatische Pausierung bei geöffneten Dialogen
- Fortsetzung wenn alle Dialoge geschlossen
- Stopp-Button funktioniert jetzt zuverlässig

Neue AJAX-Endpoints:
- searchproduct.php, addfreetextline.php
- getorders.php, getorderlines.php, updateorderline.php

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-23 09:46:06 +01:00

1750 lines
53 KiB
JavaScript
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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
// Order overview state
let 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 = '<div style="font-size: 40px; margin-bottom: 10px;">⚠️</div><div>' + message + '</div>';
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 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() {
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 = `
<div class="supplier-list">
<span class="supplier-label">${CONFIG.lang.selectSupplier}:</span>
${suppliers.map((s, i) => `
<div class="supplier-option ${i === 0 ? 'selected' : ''}" data-supplier-id="${s.id}" data-price="${s.price}" data-ref-fourn="${escapeHtml(s.ref_fourn || '')}">
<div class="supplier-radio"></div>
<div class="supplier-info">
<div class="supplier-name">${escapeHtml(s.name)}${i === 0 ? '<span class="cheapest-badge">' + CONFIG.lang.cheapest + '</span>' : ''}</div>
${s.ref_fourn ? '<div class="supplier-ref">' + CONFIG.lang.supplierOrderRef + ': ' + escapeHtml(s.ref_fourn) + '</div>' : ''}
</div>
<div class="supplier-price">${formatPrice(s.price)} &euro;</div>
</div>
`).join('')}
</div>
`;
selectedSupplier = cheapest;
} else {
// No supplier assigned - show all available suppliers
if (allSuppliers.length > 0) {
suppliersHtml = `
<div class="supplier-list">
<div class="no-supplier">${CONFIG.lang.noSupplier}</div>
<span class="supplier-label" style="margin-top: 15px; display: block;">${CONFIG.lang.selectSupplier}:</span>
${allSuppliers.map((s, i) => `
<div class="supplier-option ${i === 0 ? 'selected' : ''}" data-supplier-id="${s.id}" data-price="0">
<div class="supplier-radio"></div>
<div class="supplier-info">
<div class="supplier-name">${escapeHtml(s.name)}</div>
</div>
</div>
`).join('')}
</div>
`;
selectedSupplier = allSuppliers[0];
} else {
suppliersHtml = `<div class="no-supplier">${CONFIG.lang.noSupplier}</div>`;
}
}
elements.resultArea.innerHTML = `
<div class="order-tools">
<button type="button" class="tool-btn" id="btn-product-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
Suche
</button>
<button type="button" class="tool-btn" id="btn-freetext">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
Freitext
</button>
</div>
<div class="order-view-container" id="order-view-container">
<div class="swipe-indicator">&larr;</div>
<div class="main-content">
<div class="product-card">
<div class="product-card-header">
<div class="product-info">
<div class="product-name">${escapeHtml(product.label)}</div>
<div class="product-ref">${CONFIG.lang.ref}: ${escapeHtml(product.ref)}</div>
<div class="product-stock">${CONFIG.lang.stock}: ${product.stock} ${escapeHtml(product.stock_unit || 'Stk')}</div>
</div>
${product.barcode ? `<button type="button" class="print-barcode-btn" id="btn-print-barcode" title="Barcode drucken">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9V2h12v7M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
</button>` : ''}
</div>
</div>
${suppliersHtml}
<div class="quantity-section">
<span class="quantity-label">${CONFIG.lang.quantity}:</span>
<div class="quantity-controls">
<button type="button" class="qty-btn" id="qty-minus">&minus;</button>
<input type="number" class="qty-input" id="qty-input" value="1" min="1">
<button type="button" class="qty-btn" id="qty-plus">+</button>
</div>
</div>
<button type="button" class="action-btn btn-primary" id="add-to-order">${CONFIG.lang.add}</button>
</div>
<div class="orders-panel">
<div class="orders-panel-header">
<span class="orders-panel-title">Bestellungen</span>
<button type="button" class="orders-panel-back" id="orders-back">&rarr; Zurück</button>
</div>
<div class="orders-scroller" id="orders-scroller">
<div class="loading-spinner"></div>
</div>
</div>
<div class="order-detail-panel">
<div class="order-detail-header">
<span class="order-detail-title" id="order-detail-title">Bestellung</span>
<button type="button" class="order-detail-back" id="order-detail-back">&rarr; Zurück</button>
</div>
<div class="order-detail-content" id="order-detail-content">
</div>
</div>
</div>
`;
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;
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 = `
<div class="search-modal-content">
<div class="search-modal-header">
<input type="text" id="product-search-input" placeholder="Produkt suchen..." autofocus>
<button type="button" class="search-close-btn" id="search-close">&times;</button>
</div>
<div class="search-results" id="search-results">
<div class="search-loading" style="display:none;">Suche...</div>
</div>
</div>
`;
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 = '<div class="search-loading">Suche...</div>';
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 => `
<div class="search-result-item" data-product='${JSON.stringify(p).replace(/'/g, "&#39;")}'>
<div class="search-result-label">${escapeHtml(p.label)}</div>
<div class="search-result-info">
<span>Ref: ${escapeHtml(p.ref)}</span>
<span>Lager: ${p.stock} ${escapeHtml(p.stock_unit || 'Stk')}</span>
</div>
</div>
`).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 = '<div class="search-loading">Keine Produkte gefunden</div>';
}
})
.catch(err => {
console.error('Search error:', err);
const results = document.getElementById('search-results');
if (results) {
results.innerHTML = '<div class="search-loading">Fehler bei der Suche</div>';
}
});
}
// FREETEXT MODAL
function showFreetextModal() {
// Need a supplier for freetext
if (allSuppliers.length === 0) {
showToast('Keine Lieferanten verfügbar', 'error');
return;
}
pauseScanner();
const modal = document.createElement('div');
modal.className = 'freetext-modal';
modal.id = 'freetext-modal';
modal.innerHTML = `
<div class="freetext-modal-content">
<div class="freetext-modal-title">Freitext-Position</div>
<div class="freetext-form">
<div>
<label>Lieferant</label>
<select id="freetext-supplier">
${allSuppliers.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('')}
</select>
</div>
<div>
<label>Beschreibung</label>
<input type="text" id="freetext-desc" placeholder="z.B. Sonderartikel XYZ">
</div>
<div class="freetext-row">
<div>
<label>Anzahl</label>
<input type="number" id="freetext-qty" value="1" min="1">
</div>
<div>
<label>Preis (netto)</label>
<input type="number" id="freetext-price" value="0" min="0" step="0.01">
</div>
</div>
<div class="freetext-buttons">
<button type="button" class="action-btn btn-secondary" id="freetext-cancel">Abbrechen</button>
<button type="button" class="action-btn btn-primary" id="freetext-add">Hinzufügen</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
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;
}
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;
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 = `
<div class="orders-modal-content">
<div class="orders-panel-header">
<span class="orders-panel-title">Bestellungen</span>
<button type="button" class="search-close-btn" id="orders-modal-close">&times;</button>
</div>
<div class="orders-scroller" id="orders-modal-scroller">
<div class="loading-spinner"></div>
</div>
<div class="order-detail-section hidden" id="orders-modal-detail">
<div class="order-detail-header">
<span class="order-detail-title" id="orders-modal-detail-title">Bestellung</span>
<button type="button" class="orders-panel-back" id="orders-modal-detail-back">&larr;</button>
</div>
<div class="order-detail-content" id="orders-modal-detail-content"></div>
</div>
</div>
`;
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 = '<div class="loading-spinner"></div>';
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 = '<div class="no-orders">Fehler beim Laden</div>';
}
})
.catch(err => {
console.error('Load orders error:', err);
scroller.innerHTML = '<div class="no-orders">Fehler beim Laden</div>';
});
}
function renderOrdersInModal(orders) {
const scroller = document.getElementById('orders-modal-scroller');
if (!scroller) return;
if (orders.length === 0) {
scroller.innerHTML = '<div class="no-orders">Keine Bestellungen</div>';
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 `
<div class="order-card ${direktClass} ${isActive ? 'active' : ''}" data-order-id="${order.id}">
<div class="order-card-supplier">${escapeHtml(order.supplier_name)}</div>
<div class="order-card-ref">${escapeHtml(order.ref)}</div>
<div class="order-card-status ${statusClass}">${escapeHtml(order.status_label)}</div>
<div class="order-card-info">${order.line_count} Pos. / ${order.total_qty} Stk</div>
</div>
`;
}).join('');
// Bind click events
scroller.querySelectorAll('.order-card').forEach(card => {
card.addEventListener('click', function() {
const orderId = parseInt(this.dataset.orderId);
openOrderDetailInModal(orderId);
});
});
}
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 = '<div class="loading-spinner"></div>';
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 = '<div class="no-orders">Fehler beim Laden</div>';
}
})
.catch(err => {
console.error('Load order lines error:', err);
content.innerHTML = '<div class="no-orders">Fehler beim Laden</div>';
});
}
function renderOrderLinesInModal(lines, order, orderId) {
const content = document.getElementById('orders-modal-detail-content');
if (!content) return;
if (lines.length === 0) {
content.innerHTML = '<div class="no-orders">Keine Positionen</div>';
return;
}
const canEdit = order.status === 0;
content.innerHTML = lines.map(line => `
<div class="order-line ${canEdit ? 'editable' : ''}" data-line-id="${line.id}" data-order-id="${orderId}">
<div class="order-line-info">
<div class="order-line-label">${escapeHtml(line.product_label)}</div>
<div class="order-line-ref">${line.product_ref ? 'Ref: ' + escapeHtml(line.product_ref) : ''} ${line.stock > 0 ? '| Lager: ' + line.stock : ''}</div>
</div>
<div class="order-line-qty">${line.qty}x</div>
</div>
`).join('');
if (canEdit) {
content.querySelectorAll('.order-line').forEach(lineEl => {
lineEl.addEventListener('click', function() {
const lineId = parseInt(this.dataset.lineId);
const oId = parseInt(this.dataset.orderId);
const line = lines.find(l => l.id === lineId);
if (line) {
showLineEditDialogInModal(line, oId);
}
});
});
}
}
function showLineEditDialogInModal(line, orderId) {
pauseScanner();
const dialog = document.createElement('div');
dialog.className = 'line-edit-dialog';
dialog.id = 'line-edit-dialog';
dialog.innerHTML = `
<div class="line-edit-content">
<div class="line-edit-title">${escapeHtml(line.product_label)}</div>
<div class="line-edit-info">
<div class="line-edit-stock">Lagerbestand: <strong>${line.stock}</strong></div>
</div>
<div class="line-edit-qty">
<label>Anzahl:</label>
<input type="number" id="line-qty-input" value="${line.qty}" min="1">
</div>
<div class="line-edit-buttons">
<button type="button" class="action-btn btn-danger" id="line-delete">Löschen</button>
<button type="button" class="action-btn btn-secondary" id="line-cancel">Abbrechen</button>
<button type="button" class="action-btn btn-primary" id="line-save">Speichern</button>
</div>
</div>
`;
document.body.appendChild(dialog);
function closeDialog() {
dialog.remove();
resumeScanner();
}
document.getElementById('line-cancel').addEventListener('click', closeDialog);
document.getElementById('line-save').addEventListener('click', () => {
const newQty = parseFloat(document.getElementById('line-qty-input').value) || 1;
updateOrderLineInModal(orderId, line.id, 'update', newQty);
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) {
showLoading();
let body = `token=${CONFIG.token}&order_id=${orderId}&line_id=${lineId}&action=${action}`;
if (action === 'update') {
body += `&qty=${qty}`;
}
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 = '<div class="loading-spinner"></div>';
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 = '<div class="no-orders">Fehler beim Laden</div>';
}
})
.catch(err => {
console.error('Load orders error:', err);
scroller.innerHTML = '<div class="no-orders">Fehler beim Laden</div>';
});
}
function renderOrders(orders) {
const scroller = document.getElementById('orders-scroller');
if (!scroller) return;
if (orders.length === 0) {
scroller.innerHTML = '<div class="no-orders">Keine Bestellungen</div>';
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 `
<div class="order-card ${direktClass} ${isActive ? 'active' : ''}" data-order-id="${order.id}">
<div class="order-card-supplier">${escapeHtml(order.supplier_name)}</div>
<div class="order-card-ref">${escapeHtml(order.ref)}</div>
<div class="order-card-status ${statusClass}">${escapeHtml(order.status_label)}</div>
<div class="order-card-info">${order.line_count} Pos. / ${order.total_qty} Stk</div>
</div>
`;
}).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 = '<div class="loading-spinner"></div>';
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 = '<div class="no-orders">Fehler beim Laden</div>';
}
})
.catch(err => {
console.error('Load order lines error:', err);
content.innerHTML = '<div class="no-orders">Fehler beim Laden</div>';
});
}
function renderOrderLines(lines, order) {
const content = document.getElementById('order-detail-content');
if (!content) return;
if (lines.length === 0) {
content.innerHTML = '<div class="no-orders">Keine Positionen</div>';
return;
}
const canEdit = order.status === 0; // Only draft orders can be edited
content.innerHTML = lines.map(line => `
<div class="order-line ${canEdit ? 'editable' : ''}" data-line-id="${line.id}" data-order-id="${order.id}">
<div class="order-line-info">
<div class="order-line-label">${escapeHtml(line.product_label)}</div>
<div class="order-line-ref">${line.product_ref ? 'Ref: ' + escapeHtml(line.product_ref) : ''} ${line.stock > 0 ? '| Lager: ' + line.stock : ''}</div>
</div>
<div class="order-line-qty">${line.qty}x</div>
</div>
`).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 dialog = document.createElement('div');
dialog.className = 'line-edit-dialog';
dialog.id = 'line-edit-dialog';
dialog.innerHTML = `
<div class="line-edit-content">
<div class="line-edit-title">${escapeHtml(line.product_label)}</div>
<div class="line-edit-info">
<div class="line-edit-stock">Lagerbestand: <strong>${line.stock}</strong></div>
</div>
<div class="line-edit-qty">
<label>Anzahl:</label>
<input type="number" id="line-qty-input" value="${line.qty}" min="1">
</div>
<div class="line-edit-buttons">
<button type="button" class="action-btn btn-danger" id="line-delete">Löschen</button>
<button type="button" class="action-btn btn-secondary" id="line-cancel">Abbrechen</button>
<button type="button" class="action-btn btn-primary" id="line-save">Speichern</button>
</div>
</div>
`;
document.body.appendChild(dialog);
function closeDialog() {
dialog.remove();
resumeScanner();
}
document.getElementById('line-cancel').addEventListener('click', closeDialog);
document.getElementById('line-save').addEventListener('click', () => {
const newQty = parseFloat(document.getElementById('line-qty-input').value) || 1;
updateOrderLine(orderId, line.id, 'update', newQty);
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) {
showLoading();
let body = `token=${CONFIG.token}&order_id=${orderId}&line_id=${lineId}&action=${action}`;
if (action === 'update') {
body += `&qty=${qty}`;
}
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 `
<a href="${escapeHtml(shopUrl)}" target="_blank" rel="noopener noreferrer" class="shop-link-btn action-btn">
<span class="shop-link-name">${escapeHtml(s.name)}</span>
${s.ref_fourn ? '<span class="shop-link-ref">' + escapeHtml(s.ref_fourn) + '</span>' : ''}
<span class="shop-link-arrow">&rarr;</span>
</a>
`;
}
return `
<div class="shop-link-btn action-btn" style="opacity: 0.5; cursor: not-allowed;">
<span class="shop-link-name">${escapeHtml(s.name)}</span>
<span class="shop-link-arrow">&mdash;</span>
</div>
`;
}).join('');
} else {
linksHtml = `<div class="no-supplier">${CONFIG.lang.noSupplier}</div>`;
}
elements.resultArea.innerHTML = `
<div class="product-card">
<div class="product-name">${escapeHtml(product.label)}</div>
<div class="product-ref">${CONFIG.lang.ref}: ${escapeHtml(product.ref)}</div>
<div class="product-stock">${CONFIG.lang.stock}: ${product.stock} ${escapeHtml(product.stock_unit || 'Stk')}</div>
</div>
<div class="shop-links">
<span class="supplier-label">${CONFIG.lang.openShop}:</span>
${linksHtml}
</div>
`;
}
// INVENTORY MODE
function showInventoryMode(product) {
elements.resultArea.innerHTML = `
<div class="product-card">
<div class="product-name">${escapeHtml(product.label)}</div>
<div class="product-ref">${CONFIG.lang.ref}: ${escapeHtml(product.ref)}</div>
</div>
<div class="stock-display">
<div class="stock-item">
<div class="stock-value" id="current-stock">${product.stock}</div>
<div class="stock-label">${CONFIG.lang.currentStock}</div>
</div>
<div class="stock-arrow">&rarr;</div>
<div class="stock-item">
<div class="stock-value" id="new-stock-preview">${product.stock}</div>
<div class="stock-label">${CONFIG.lang.newStock}</div>
</div>
</div>
<div class="quantity-section">
<span class="quantity-label">${CONFIG.lang.newStock}:</span>
<div class="quantity-controls">
<button type="button" class="qty-btn" id="stock-minus">&minus;</button>
<input type="number" class="qty-input" id="stock-input" value="${product.stock}" min="0">
<button type="button" class="qty-btn" id="stock-plus">+</button>
</div>
</div>
<button type="button" class="action-btn btn-primary" id="save-stock">${CONFIG.lang.save}</button>
`;
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 = `
<div class="confirm-content">
<div class="confirm-title">${CONFIG.lang.confirmStockChange}</div>
<div class="confirm-message">${oldStock} &rarr; ${newStock}</div>
<div class="confirm-buttons">
<button type="button" class="action-btn btn-secondary" id="confirm-cancel">${CONFIG.lang.cancel}</button>
<button type="button" class="action-btn btn-primary" id="confirm-ok">${CONFIG.lang.confirm}</button>
</div>
</div>
`;
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 = `
<div class="error-message">
<div class="error-icon">&#10060;</div>
<div class="error-text">${CONFIG.lang.productNotFound}</div>
<div class="product-ref" style="margin-top: 10px;">${escapeHtml(barcode)}</div>
</div>
`;
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: '<div class="spinner"></div>' });
}
}
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();
}
})();