v6.8: Stopp-Button Fix, persistente Freitext-Lieferant-Speicherung, SW-Update

- 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>
This commit is contained in:
Eduard Wisch 2026-03-01 21:50:36 +01:00
parent 08221a660d
commit d81f215f59
4 changed files with 107 additions and 21 deletions

View file

@ -76,7 +76,7 @@ class modHandyBarcodeScanner extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@handybarcodescanner' $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@handybarcodescanner'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '6.0'; $this->version = '6.8';
// Url to the file with your last numberversion of this module // Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt'; //$this->url_last_version = 'http://www.example.com/versionmodule.txt';

View file

@ -46,10 +46,40 @@
let allSuppliers = []; let allSuppliers = [];
let quaggaInitialized = false; let quaggaInitialized = false;
let openDialogCount = 0; // Zaehlt offene Dialoge let openDialogCount = 0; // Zaehlt offene Dialoge
let lastFreetextSupplierId = null; // Zuletzt verwendeter Freitext-Lieferant
// Order overview state // Persistente Einstellungen - eine zentrale Config statt einzelner Keys
let lastOrderId = null; 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 cachedOrders = [];
let currentOrderDetail = null; let currentOrderDetail = null;
@ -277,10 +307,15 @@
Quagga.start(); Quagga.start();
quaggaInitialized = true; quaggaInitialized = true;
isScanning = true; isScanning = true;
elements.startBtn.disabled = false;
elements.startBtn.classList.add('hidden'); elements.startBtn.classList.add('hidden');
elements.stopBtn.classList.remove('hidden'); elements.stopBtn.classList.remove('hidden');
elements.videoContainer.classList.add('scanning'); 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 // Register detection handler
Quagga.onDetected(onBarcodeDetected); Quagga.onDetected(onBarcodeDetected);
@ -728,6 +763,7 @@
showToast(`${CONFIG.lang.added}: ${productName} (${qty}x)`, 'success'); showToast(`${CONFIG.lang.added}: ${productName} (${qty}x)`, 'success');
// Save last order ID for auto-open in order view // Save last order ID for auto-open in order view
lastOrderId = data.order_id; lastOrderId = data.order_id;
saveConfig('lastOrderId', data.order_id);
hideResult(); hideResult();
} else { } else {
showToast(data.error || CONFIG.lang.error, 'error'); showToast(data.error || CONFIG.lang.error, 'error');
@ -856,6 +892,8 @@
pauseScanner(); pauseScanner();
const savedSupplierId = loadConfig().lastFreetextSupplierId;
const modal = document.createElement('div'); const modal = document.createElement('div');
modal.className = 'freetext-modal'; modal.className = 'freetext-modal';
modal.id = 'freetext-modal'; modal.id = 'freetext-modal';
@ -866,7 +904,7 @@
<div> <div>
<label>Lieferant</label> <label>Lieferant</label>
<select id="freetext-supplier"> <select id="freetext-supplier">
${allSuppliers.map(s => `<option value="${s.id}" ${s.id == lastFreetextSupplierId ? 'selected' : ''}>${escapeHtml(s.name)}</option>`).join('')} ${allSuppliers.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('')}
</select> </select>
</div> </div>
<div> <div>
@ -892,6 +930,17 @@
`; `;
document.body.appendChild(modal); 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() { function closeModal() {
modal.remove(); modal.remove();
resumeScanner(); resumeScanner();
@ -923,8 +972,9 @@
return; return;
} }
// Lieferant merken für nächstes Mal // Lieferant merken für nächstes Mal (auch über Neustarts hinweg)
lastFreetextSupplierId = supplierId; lastFreetextSupplierId = supplierId;
saveConfig('lastFreetextSupplierId', supplierId);
showLoading(); showLoading();
@ -942,6 +992,7 @@
if (data.success) { if (data.success) {
showToast(`Hinzugefügt: ${description} (${qty}x)`, 'success'); showToast(`Hinzugefügt: ${description} (${qty}x)`, 'success');
lastOrderId = data.order_id; lastOrderId = data.order_id;
saveConfig('lastOrderId', data.order_id);
if (closeModalCallback) closeModalCallback(); if (closeModalCallback) closeModalCallback();
} else { } else {
showToast(data.error || CONFIG.lang.error, 'error'); showToast(data.error || CONFIG.lang.error, 'error');

33
pwa.php
View file

@ -53,7 +53,7 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
<title>Barcode Scanner</title> <title>Barcode Scanner</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" href="img/icon-192.png"> <link rel="apple-touch-icon" href="img/icon-192.png">
<link rel="stylesheet" href="css/scanner.css?v=60"> <link rel="stylesheet" href="css/scanner.css?v=61">
<style> <style>
* { * {
box-sizing: border-box; box-sizing: border-box;
@ -314,6 +314,7 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
aspect-ratio: 4/3; aspect-ratio: 4/3;
max-height: 35vh; max-height: 35vh;
background: #000; background: #000;
overflow: hidden;
} }
#scanner-video { #scanner-video {
@ -351,26 +352,44 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 10px; gap: 10px;
position: relative;
z-index: 10;
} }
.scan-btn { .scan-btn {
padding: 10px 20px; padding: 12px 28px;
font-size: 14px; font-size: 15px;
font-weight: 600; font-weight: 700;
border: none; border: none;
border-radius: 6px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
letter-spacing: 0.3px;
text-transform: uppercase;
}
.scan-btn:active {
transform: scale(0.95);
filter: brightness(1.2);
}
.scan-btn:disabled {
opacity: 0.5;
cursor: wait;
transform: none;
filter: none;
} }
.scan-btn.start { .scan-btn.start {
background: var(--primary); background: var(--primary);
color: white; color: white;
box-shadow: 0 2px 8px rgba(0, 119, 179, 0.4);
} }
.scan-btn.stop { .scan-btn.stop {
background: var(--danger); background: #e53935;
color: white; color: white;
box-shadow: 0 2px 8px rgba(229, 57, 53, 0.4);
} }
/* Tool buttons next to scan button */ /* Tool buttons next to scan button */
@ -1280,6 +1299,6 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
<script src="<?php echo dol_buildpath('/handybarcodescanner/js/scanner.js', 1); ?>?v=60"></script> <script src="<?php echo dol_buildpath('/handybarcodescanner/js/scanner.js', 1); ?>?v=61"></script>
</body> </body>
</html> </html>

20
sw.js
View file

@ -1,5 +1,5 @@
// Service Worker for HandyBarcodeScanner PWA // Service Worker for HandyBarcodeScanner PWA
const CACHE_NAME = 'scanner-v6.0'; const CACHE_NAME = 'scanner-v6.1';
const ASSETS = [ const ASSETS = [
'pwa.php', 'pwa.php',
'css/scanner.css', 'css/scanner.css',
@ -60,7 +60,11 @@ self.addEventListener('fetch', event => {
return; return;
} }
// Cache first for static assets // Eigene Assets: Network first, Cache als Fallback (damit Updates sofort ankommen)
// CDN-Assets: Cache first (aendern sich nie dank versionierter URL)
const isCDN = url.hostname !== location.hostname;
if (isCDN) {
event.respondWith( event.respondWith(
caches.match(event.request).then(cached => { caches.match(event.request).then(cached => {
return cached || fetch(event.request).then(response => { return cached || fetch(event.request).then(response => {
@ -72,6 +76,18 @@ self.addEventListener('fetch', event => {
}); });
}) })
); );
} else {
// Network first fuer eigene JS/CSS/Bilder
event.respondWith(
fetch(event.request).then(response => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
}
return response;
}).catch(() => caches.match(event.request))
);
}
}); });
// Listen for messages from main app // Listen for messages from main app