- Stopp-Button: overflow:hidden auf Video-Container, z-index auf Controls, pointer-events:none auf Quagga-Canvas damit der Button klickbar bleibt - Freitext-Lieferant wird persistent in localStorage gespeichert (hbs_config) und bei Dropdown-Aenderung sofort aktualisiert - Service Worker: Network-first fuer eigene Assets (JS/CSS), Cache-first nur noch fuer CDN-Libraries - Letzte Bestell-ID (lastOrderId) ebenfalls persistent - Migration alter localStorage-Keys in neue zentrale Config Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1914 lines
58 KiB
JavaScript
Executable file
1914 lines
58 KiB
JavaScript
Executable file
/**
|
||
* 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 = '<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.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 = `
|
||
<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)} €</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">←</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">−</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">→ 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">→ 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;
|
||
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 = `
|
||
<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">×</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, "'")}'>
|
||
<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 savedSupplierId = loadConfig().lastFreetextSupplierId;
|
||
|
||
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);
|
||
|
||
// 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 = `
|
||
<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">×</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">←</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' : '';
|
||
const canDelete = order.status === 0; // Nur Entwürfe können gelöscht werden
|
||
|
||
return `
|
||
<div class="order-card ${direktClass} ${isActive ? 'active' : ''}" data-order-id="${order.id}">
|
||
${canDelete ? `<button type="button" class="order-delete-btn" data-order-id="${order.id}" title="Bestellung löschen">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
|
||
</button>` : ''}
|
||
<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 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 = `
|
||
<div class="line-edit-content">
|
||
<div class="line-edit-title">Bestellung löschen?</div>
|
||
<div class="line-edit-info" style="text-align:center; margin: 15px 0;">
|
||
<strong>${escapeHtml(orderRef)}</strong><br>
|
||
<span style="color: var(--danger, #993013);">Diese Aktion kann nicht rückgängig gemacht werden.</span>
|
||
</div>
|
||
<div class="line-edit-buttons">
|
||
<button type="button" class="action-btn btn-secondary" id="delete-order-cancel">Abbrechen</button>
|
||
<button type="button" class="action-btn btn-danger" id="delete-order-confirm">Löschen</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 = '<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>';
|
||
});
|
||
}
|
||
|
||
// 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 = '<div class="no-orders">Keine Positionen</div>';
|
||
return;
|
||
}
|
||
|
||
const canEdit = order.status == 0;
|
||
|
||
content.innerHTML = lines.map(line => `
|
||
<div class="order-line" data-line-id="${line.id}" data-order-id="${orderId}" data-is-freetext="${line.is_freetext ? '1' : '0'}">
|
||
<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.is_freetext ? '(Freitext)' : (line.stock > 0 ? '| Lager: ' + line.stock : '')}</div>
|
||
</div>
|
||
<div class="order-line-qty">${line.qty}x</div>
|
||
${canEdit ? `<button type="button" class="order-line-edit-btn" data-line-id="${line.id}" title="Bearbeiten">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||
</button>` : ''}
|
||
</div>
|
||
`).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 = `
|
||
<div class="line-edit-content">
|
||
<div class="line-edit-title">${isFreetext ? 'Freitext-Position' : escapeHtml(line.product_label)}</div>
|
||
${isFreetext ? `
|
||
<div class="line-edit-desc">
|
||
<label>Beschreibung:</label>
|
||
<input type="text" id="line-desc-input" value="${escapeHtml(line.description || 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-primary" id="line-save">Speichern</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 = '<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 isFreetext = line.is_freetext || !line.product_id;
|
||
|
||
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">${isFreetext ? 'Freitext-Position' : escapeHtml(line.product_label)}</div>
|
||
${isFreetext ? `
|
||
<div class="line-edit-desc">
|
||
<label>Beschreibung:</label>
|
||
<input type="text" id="line-desc-input" value="${escapeHtml(line.description || 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-primary" id="line-save">Speichern</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 `
|
||
<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">→</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">—</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">→</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">−</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} → ${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">❌</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();
|
||
}
|
||
})();
|