extractDigitSequences() Fix: - VORHER: Alle Ziffern zusammengefasst → Datensalat bei Subsequenzen - JETZT: Nur isolierte N-stellige Zahlenblöcke (filter auf length === N) - Beispiel: "Artikel 1234567 Datum 20250311" → nur "1234567" (nicht "2025031" etc.) Artikelnummer-Suche: - findproduct.php sucht in: barcode, ref_fourn (Lieferanten-Artikelnummer!), ref - OCR-erkannte Nummern funktionieren wenn ref_fourn in Dolibarr gepflegt ist Dokumentation: - README.md aktualisiert: ZXing-JS, OCR-Feature, Versionierung - Barcode-Unterstützung erweitert: Code 128, QR, DataMatrix, OCR - Bibliotheken: ZXing-JS v0.21.3, Tesseract.js v5.1.0 Versionen synchron erhöht: - pwa.php: ?v=91 (CSS + JS) - sw.js: scanner-v9.1 - modHandyBarcodeScanner.class.php: 9.1 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1499 lines
51 KiB
PHP
Executable file
1499 lines
51 KiB
PHP
Executable file
<?php
|
|
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
|
*
|
|
* PWA Standalone Scanner - Mit eigenem Login (15 Tage gespeichert)
|
|
*/
|
|
|
|
// Kein Login erforderlich - wird via JavaScript geprueft
|
|
if (!defined('NOLOGIN')) {
|
|
define('NOLOGIN', '1');
|
|
}
|
|
if (!defined('NOREQUIREMENU')) {
|
|
define('NOREQUIREMENU', '1');
|
|
}
|
|
|
|
// Load Dolibarr environment
|
|
$res = 0;
|
|
if (!$res && file_exists("../main.inc.php")) {
|
|
$res = @include "../main.inc.php";
|
|
}
|
|
if (!$res && file_exists("../../main.inc.php")) {
|
|
$res = @include "../../main.inc.php";
|
|
}
|
|
if (!$res && file_exists("../../../main.inc.php")) {
|
|
$res = @include "../../../main.inc.php";
|
|
}
|
|
if (!$res) {
|
|
die("Dolibarr konnte nicht geladen werden");
|
|
}
|
|
|
|
// Load translation files
|
|
$langs->loadLangs(array("handybarcodescanner@handybarcodescanner", "products", "orders", "stocks"));
|
|
|
|
// Get parameters
|
|
$mode = GETPOST('mode', 'alpha') ?: 'order';
|
|
|
|
// Check mode-specific permissions
|
|
$enableOrder = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_ORDER', 1);
|
|
$enableShop = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_SHOP', 1);
|
|
$enableInventory = getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_INVENTORY', 1);
|
|
|
|
// Get Dolibarr theme colors
|
|
$colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
|
|
|
|
?><!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<meta name="theme-color" content="<?php echo $colormain; ?>">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<meta name="apple-mobile-web-app-title" content="Scanner">
|
|
<title>Barcode Scanner</title>
|
|
<link rel="manifest" href="manifest.json">
|
|
<link rel="apple-touch-icon" href="img/icon-192.png">
|
|
<link rel="stylesheet" href="css/scanner.css?v=91">
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
:root {
|
|
--primary: <?php echo $colormain; ?>;
|
|
--colorbackbody: #1d1e20;
|
|
--colorbackcard: #1d1e20;
|
|
--colorbacktitle: #3b3c3e;
|
|
--colorbackline: #38393d;
|
|
--colorbackinput: rgb(70, 70, 70);
|
|
--colortext: rgb(220,220,220);
|
|
--colortextmuted: rgb(180,180,180);
|
|
--colortextlink: #4390dc;
|
|
--colorborder: #2b2c2e;
|
|
--butactionbg: rgb(173,140,79);
|
|
--textbutaction: rgb(255,255,255);
|
|
--success: #25a580;
|
|
--danger: #993013;
|
|
--warning: #bc9526;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
background: var(--colorbackbody);
|
|
color: var(--colortext);
|
|
min-height: 100vh;
|
|
min-height: 100dvh;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.app {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
min-height: 100dvh;
|
|
}
|
|
|
|
/* Login Screen */
|
|
.login-screen {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.login-box {
|
|
width: 100%;
|
|
max-width: 340px;
|
|
background: var(--colorbackline);
|
|
border-radius: 12px;
|
|
padding: 30px 24px;
|
|
border: 1px solid var(--colorborder);
|
|
}
|
|
|
|
.login-logo {
|
|
text-align: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.login-logo svg {
|
|
width: 64px;
|
|
height: 64px;
|
|
fill: var(--primary);
|
|
}
|
|
|
|
.login-title {
|
|
text-align: center;
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.login-subtitle {
|
|
text-align: center;
|
|
font-size: 14px;
|
|
color: var(--colortextmuted);
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.login-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.form-label {
|
|
font-size: 14px;
|
|
color: var(--colortextmuted);
|
|
}
|
|
|
|
.form-input {
|
|
padding: 14px 16px;
|
|
font-size: 16px;
|
|
background: var(--colorbackinput);
|
|
border: 1px solid var(--colorborder);
|
|
border-radius: 8px;
|
|
color: var(--colortext);
|
|
outline: none;
|
|
}
|
|
|
|
.form-input:focus {
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.remember-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.remember-row input[type="checkbox"] {
|
|
width: 20px;
|
|
height: 20px;
|
|
accent-color: var(--primary);
|
|
}
|
|
|
|
.remember-row label {
|
|
font-size: 14px;
|
|
color: var(--colortextmuted);
|
|
}
|
|
|
|
.login-btn {
|
|
padding: 16px;
|
|
font-size: 17px;
|
|
font-weight: 600;
|
|
background: var(--butactionbg);
|
|
color: var(--textbutaction);
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.login-btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.login-error {
|
|
background: var(--danger);
|
|
color: white;
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
display: none;
|
|
}
|
|
|
|
.login-info {
|
|
background: var(--colorbacktitle);
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
color: var(--colortextmuted);
|
|
text-align: center;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
background: var(--primary);
|
|
color: #fff;
|
|
padding: 14px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
|
|
.header-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.header-user {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.header-username {
|
|
font-size: 13px;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.header-logout {
|
|
background: rgba(255,255,255,0.2);
|
|
border: none;
|
|
color: white;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Mode Tabs */
|
|
.mode-tabs {
|
|
display: flex;
|
|
background: var(--colorbacktitle);
|
|
border-bottom: 1px solid var(--colorborder);
|
|
}
|
|
|
|
.mode-tab {
|
|
flex: 1;
|
|
padding: 14px 8px;
|
|
text-align: center;
|
|
color: var(--colortextmuted);
|
|
text-decoration: none;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
border-bottom: 3px solid transparent;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
background: none;
|
|
border-left: none;
|
|
border-right: none;
|
|
border-top: none;
|
|
}
|
|
|
|
.mode-tab.active {
|
|
color: var(--primary);
|
|
border-bottom-color: var(--primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Scanner Area */
|
|
.scanner-area {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 12px;
|
|
}
|
|
|
|
.scanner-box {
|
|
background: #333;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.scanner-video-box {
|
|
position: relative;
|
|
width: 100%;
|
|
aspect-ratio: 4/3;
|
|
max-height: 35vh;
|
|
background: #000;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#scanner-video {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.scan-region-highlight {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 70%;
|
|
height: 50%;
|
|
border: 3px solid var(--primary);
|
|
border-radius: 8px;
|
|
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.scanner-video-box.scanning .scan-region-highlight {
|
|
animation: pulse 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { border-color: var(--primary); }
|
|
50% { border-color: var(--success); }
|
|
}
|
|
|
|
.scanner-controls {
|
|
padding: 8px 10px;
|
|
background: #444;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
position: relative;
|
|
z-index: 10;
|
|
}
|
|
|
|
.scan-btn {
|
|
padding: 12px 28px;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
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 {
|
|
background: var(--primary);
|
|
color: white;
|
|
box-shadow: 0 2px 8px rgba(0, 119, 179, 0.4);
|
|
}
|
|
|
|
.scan-btn.stop {
|
|
background: #e53935;
|
|
color: white;
|
|
box-shadow: 0 2px 8px rgba(229, 57, 53, 0.4);
|
|
}
|
|
|
|
/* Tool buttons next to scan button */
|
|
.scan-tool-btn {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: var(--colorbackline);
|
|
color: var(--colortext);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s;
|
|
}
|
|
.scan-tool-btn:active { transform: scale(0.95); opacity: 0.8; }
|
|
.scan-tool-btn svg { width: 22px; height: 22px; }
|
|
.scan-tool-btn.search-btn { order: 1; }
|
|
.barcode-type-selector { order: 2; padding: 5px 10px; width: 100%; }
|
|
.scan-buttons-group { order: 3; display: flex; gap: 8px; }
|
|
.scan-tool-btn.freetext-btn { order: 4; }
|
|
.scan-tool-btn.history-btn { order: 5; }
|
|
|
|
.barcode-type-dropdown {
|
|
width: 100%;
|
|
padding: 12px;
|
|
font-size: 15px;
|
|
background: var(--colorbackinput);
|
|
color: var(--colortext);
|
|
border: 1px solid var(--colorborder);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
}
|
|
.barcode-type-dropdown:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
/* OCR Digit Selector */
|
|
.ocr-digit-selector {
|
|
padding: 5px 10px 10px 10px;
|
|
background: #444;
|
|
}
|
|
.ocr-digit-selector.hidden {
|
|
display: none;
|
|
}
|
|
.ocr-digit-dropdown {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
padding: 14px 20px;
|
|
}
|
|
|
|
/* OCR Loading Overlay */
|
|
.ocr-loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.8);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 9999;
|
|
}
|
|
.ocr-loading-spinner {
|
|
width: 50px;
|
|
height: 50px;
|
|
border: 4px solid rgba(255,255,255,0.2);
|
|
border-top-color: var(--primary);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
.ocr-loading-text {
|
|
color: #fff;
|
|
margin-top: 16px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* OCR Selection Modal */
|
|
.ocr-select-modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.8);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 9999;
|
|
padding: 20px;
|
|
}
|
|
.ocr-select-content {
|
|
background: var(--colorbackbody);
|
|
border-radius: 8px;
|
|
max-width: 400px;
|
|
width: 100%;
|
|
max-height: 80vh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.ocr-select-header {
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--colorborder);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.ocr-select-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: 24px;
|
|
color: var(--colortext);
|
|
cursor: pointer;
|
|
padding: 0;
|
|
width: 30px;
|
|
height: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.ocr-select-list {
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
}
|
|
.ocr-select-item {
|
|
padding: 16px;
|
|
background: var(--colorbackinput);
|
|
border: 1px solid var(--colorborder);
|
|
border-radius: 6px;
|
|
margin-bottom: 8px;
|
|
cursor: pointer;
|
|
font-family: monospace;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
transition: all 0.2s;
|
|
}
|
|
.ocr-select-item:hover {
|
|
background: var(--butactionbg);
|
|
color: var(--textbutaction);
|
|
border-color: var(--butactionbg);
|
|
}
|
|
.ocr-select-item:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
/* Format Badge (im Last Scan) */
|
|
.scan-format-badge {
|
|
display: inline-block;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
background: var(--butactionbg);
|
|
color: var(--textbutaction);
|
|
margin-right: 8px;
|
|
}
|
|
.scan-code-value {
|
|
font-family: monospace;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Swipe hint */
|
|
.swipe-hint {
|
|
position: fixed;
|
|
right: 8px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
background: rgba(0,0,0,0.6);
|
|
color: var(--colortextmuted);
|
|
padding: 8px 12px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
pointer-events: none;
|
|
animation: swipeHintPulse 2s ease-in-out infinite;
|
|
z-index: 50;
|
|
}
|
|
@keyframes swipeHintPulse {
|
|
0%, 100% { opacity: 0.7; transform: translateY(-50%) translateX(0); }
|
|
50% { opacity: 1; transform: translateY(-50%) translateX(-5px); }
|
|
}
|
|
|
|
/* Last Scan */
|
|
.scanner-last-scan {
|
|
background: var(--colorbackline);
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
margin-bottom: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
border: 1px solid var(--colorborder);
|
|
}
|
|
|
|
.last-scan-label {
|
|
color: var(--colortextmuted);
|
|
font-size: 14px;
|
|
}
|
|
|
|
#last-scan-code {
|
|
font-family: monospace;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--colortextlink);
|
|
}
|
|
|
|
#result-area {
|
|
flex: 1;
|
|
}
|
|
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Toast */
|
|
.scanner-toast {
|
|
position: fixed;
|
|
bottom: 80px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
padding: 14px 28px;
|
|
border-radius: 10px;
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
z-index: 10000;
|
|
}
|
|
|
|
.scanner-toast.success {
|
|
background: var(--success);
|
|
color: white;
|
|
}
|
|
|
|
.scanner-toast.error {
|
|
background: var(--danger);
|
|
color: white;
|
|
}
|
|
|
|
/* Loading Overlay */
|
|
.loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: var(--colorbackbody);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 50px;
|
|
height: 50px;
|
|
border: 4px solid var(--colorborder);
|
|
border-top-color: var(--primary);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Dolibarr Dark Theme Overrides */
|
|
.product-card { background: var(--colorbackline); border: 1px solid var(--colorborder); color: var(--colortext); }
|
|
.product-name { color: var(--colortext); }
|
|
.product-ref { color: var(--colortextmuted); }
|
|
.product-stock { background: var(--colorbacktitle); color: var(--colortext); }
|
|
.supplier-option { background: var(--colorbackline); border: 2px solid var(--colorborder); color: var(--colortext); }
|
|
.supplier-option.selected { border-color: var(--butactionbg); background: rgba(173, 140, 79, 0.15); }
|
|
.supplier-name { color: var(--colortext); }
|
|
.supplier-price { color: var(--colortextlink); }
|
|
.qty-btn { background: var(--colorbacktitle); border: 1px solid var(--colorborder); color: var(--colortext); }
|
|
.qty-input { background: var(--colorbackinput); border: 1px solid var(--colorborder); color: var(--colortext); }
|
|
.action-btn { width: 100%; padding: 16px; font-size: 17px; font-weight: 600; border: none; border-radius: 3px; cursor: pointer; margin-top: 16px; text-transform: uppercase; }
|
|
.action-btn.btn-primary, .btn-primary { background: var(--butactionbg) !important; color: var(--textbutaction) !important; }
|
|
.stock-display { background: var(--colorbackline); border: 1px solid var(--colorborder); }
|
|
.stock-value { color: var(--colortextlink); }
|
|
.shop-link-btn { background: var(--colorbacktitle); border: 1px solid var(--colorborder); color: var(--colortext); }
|
|
.confirm-dialog { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 10000; padding: 20px; }
|
|
.confirm-content { background: var(--colorbackline); border-radius: 8px; padding: 24px; width: 100%; max-width: 320px; text-align: center; border: 1px solid var(--colorborder); }
|
|
.confirm-title { font-size: 18px; font-weight: 600; margin-bottom: 12px; color: var(--colortext); }
|
|
.confirm-message { font-size: 14px; color: var(--colortextmuted); margin-bottom: 24px; }
|
|
.confirm-buttons { display: flex; gap: 12px; }
|
|
.confirm-buttons .action-btn { flex: 1; margin-top: 0; }
|
|
.action-btn.btn-secondary, .btn-secondary { background: var(--colorbacktitle) !important; color: var(--colortext) !important; border: 1px solid var(--colorborder); }
|
|
.action-btn.btn-danger, .btn-danger { background: var(--danger) !important; color: white !important; }
|
|
|
|
/* Order Tools - Search & Freetext buttons */
|
|
.order-tools { display: flex; gap: 8px; margin-bottom: 12px; }
|
|
.tool-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 12px; background: var(--colorbackline); border: 1px solid var(--colorborder); border-radius: 8px; font-size: 14px; color: var(--colortext); cursor: pointer; }
|
|
.tool-btn:active { transform: scale(0.98); opacity: 0.8; }
|
|
.tool-btn svg { width: 18px; height: 18px; }
|
|
|
|
/* Search Modal Dark */
|
|
.search-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: flex-start; justify-content: center; z-index: 10000; padding: 15px; padding-top: 50px; }
|
|
.search-modal-content { background: var(--colorbackline); border-radius: 12px; width: 100%; max-width: 400px; max-height: 80vh; display: flex; flex-direction: column; border: 1px solid var(--colorborder); }
|
|
.search-modal-header { padding: 12px; border-bottom: 1px solid var(--colorborder); display: flex; align-items: center; gap: 10px; }
|
|
.search-modal-header input { flex: 1; padding: 12px; border: 1px solid var(--colorborder); border-radius: 6px; font-size: 16px; background: var(--colorbackinput); color: var(--colortext); }
|
|
.search-close-btn { padding: 8px 12px; background: none; border: none; font-size: 22px; cursor: pointer; color: var(--colortextmuted); }
|
|
.search-results { flex: 1; overflow-y: auto; padding: 10px; }
|
|
.search-result-item { padding: 14px; border: 1px solid var(--colorborder); border-radius: 8px; margin-bottom: 8px; cursor: pointer; background: var(--colorbacktitle); }
|
|
.search-result-item:active { background: var(--colorbackline); }
|
|
.search-result-label { font-weight: 600; font-size: 15px; color: var(--colortext); margin-bottom: 4px; }
|
|
.search-result-info { font-size: 13px; color: var(--colortextmuted); display: flex; gap: 15px; }
|
|
.search-loading { text-align: center; padding: 20px; color: var(--colortextmuted); }
|
|
|
|
/* Freetext Modal Dark */
|
|
.freetext-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000; padding: 15px; }
|
|
.freetext-modal-content { background: var(--colorbackline); border-radius: 12px; padding: 20px; width: 100%; max-width: 380px; border: 1px solid var(--colorborder); }
|
|
.freetext-modal-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: var(--colortext); }
|
|
.freetext-form { display: flex; flex-direction: column; gap: 14px; }
|
|
.freetext-form label { font-size: 13px; color: var(--colortextmuted); margin-bottom: 4px; display: block; }
|
|
.freetext-form input, .freetext-form select { width: 100%; padding: 12px; border: 1px solid var(--colorborder); border-radius: 6px; font-size: 16px; background: var(--colorbackinput); color: var(--colortext); box-sizing: border-box; }
|
|
.freetext-row { display: flex; gap: 10px; }
|
|
.freetext-row > div { flex: 1; }
|
|
.freetext-buttons { display: flex; gap: 10px; margin-top: 8px; }
|
|
.freetext-buttons .action-btn { flex: 1; margin-top: 0; }
|
|
|
|
/* Swipeable Order Overview */
|
|
.order-view-container { position: relative; overflow: hidden; width: 100%; }
|
|
.swipe-indicator { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); padding: 10px; background: rgba(255,255,255,0.1); border-radius: 50%; font-size: 18px; color: var(--colortextmuted); pointer-events: none; opacity: 0.7; animation: swipeHint 2s ease-in-out infinite; z-index: 5; }
|
|
@keyframes swipeHint { 0%, 100% { transform: translateY(-50%) translateX(0); } 50% { transform: translateY(-50%) translateX(-6px); } }
|
|
.order-view-container.swiped .swipe-indicator { display: none; }
|
|
|
|
/* Orders Panel */
|
|
.orders-panel { position: absolute; top: 0; left: 100%; width: 100%; height: 100%; background: var(--colorbackbody); transition: transform 0.3s ease; overflow: hidden; display: flex; flex-direction: column; }
|
|
.order-view-container.swiped .orders-panel { transform: translateX(-100%); }
|
|
.orders-panel-header { padding: 14px; background: var(--colorbacktitle); border-bottom: 1px solid var(--colorborder); display: flex; align-items: center; justify-content: space-between; }
|
|
.orders-panel-title { font-weight: 600; font-size: 16px; color: var(--colortext); }
|
|
.orders-panel-back { padding: 8px 14px; background: var(--colorbackline); border: 1px solid var(--colorborder); border-radius: 6px; font-size: 14px; cursor: pointer; color: var(--colortext); }
|
|
|
|
/* Order Scroller */
|
|
.orders-scroller { flex: 1; overflow-x: auto; overflow-y: hidden; white-space: nowrap; padding: 15px; -webkit-overflow-scrolling: touch; }
|
|
.order-card { display: inline-block; vertical-align: top; width: 170px; min-height: 110px; padding: 14px; margin-right: 12px; background: var(--colorbackline); border: 2px solid var(--colorborder); border-radius: 10px; cursor: pointer; white-space: normal; }
|
|
.order-card.active { border-color: var(--butactionbg); background: rgba(173,140,79,0.15); }
|
|
.order-card.direkt { font-weight: bold; }
|
|
.order-card.direkt .order-card-ref { font-weight: 700; }
|
|
.order-card-supplier { font-size: 15px; font-weight: 600; margin-bottom: 6px; color: var(--colortext); overflow: hidden; text-overflow: ellipsis; }
|
|
.order-card-ref { font-size: 13px; color: var(--colortextmuted); margin-bottom: 6px; }
|
|
.order-card-status { display: inline-block; font-size: 11px; padding: 3px 8px; border-radius: 4px; background: var(--colorbacktitle); color: var(--colortextmuted); }
|
|
.order-card-status.draft { background: rgba(188,149,38,0.3); color: var(--warning); }
|
|
.order-card-status.validated { background: rgba(37,165,128,0.3); color: var(--success); }
|
|
.order-card-info { margin-top: 10px; font-size: 12px; color: var(--colortextmuted); }
|
|
|
|
/* Order Detail Panel */
|
|
.order-detail-panel { position: absolute; top: 0; left: 200%; width: 100%; height: 100%; background: var(--colorbackbody); transition: transform 0.3s ease; overflow: hidden; display: flex; flex-direction: column; }
|
|
.order-view-container.detail-open .order-detail-panel { transform: translateX(-200%); }
|
|
.order-detail-header { padding: 14px; background: var(--colorbacktitle); border-bottom: 1px solid var(--colorborder); display: flex; align-items: center; justify-content: space-between; }
|
|
.order-detail-title { font-weight: 600; font-size: 15px; color: var(--colortext); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
.order-detail-back { padding: 8px 14px; background: var(--colorbackline); border: 1px solid var(--colorborder); border-radius: 6px; font-size: 14px; cursor: pointer; color: var(--colortext); margin-left: 10px; }
|
|
.order-detail-content { flex: 1; overflow-y: auto; padding: 15px; }
|
|
|
|
/* Order Lines */
|
|
.order-line { display: flex; align-items: center; justify-content: space-between; padding: 14px; background: var(--colorbackline); border: 1px solid var(--colorborder); border-radius: 8px; margin-bottom: 10px; cursor: pointer; }
|
|
.order-line:active { background: var(--colorbacktitle); }
|
|
.order-line-info { flex: 1; min-width: 0; }
|
|
.order-line-label { font-size: 15px; font-weight: 500; color: var(--colortext); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.order-line-ref { font-size: 12px; color: var(--colortextmuted); margin-top: 3px; }
|
|
.order-line-qty { font-size: 18px; font-weight: 700; color: var(--colortextlink); margin-left: 15px; white-space: nowrap; }
|
|
|
|
/* Line Edit Dialog */
|
|
.line-edit-dialog { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000; padding: 15px; }
|
|
.line-edit-content { background: var(--colorbackline); border-radius: 12px; padding: 20px; width: 100%; max-width: 340px; border: 1px solid var(--colorborder); }
|
|
.line-edit-title { font-size: 17px; font-weight: 600; margin-bottom: 16px; color: var(--colortext); }
|
|
.line-edit-info { background: var(--colorbacktitle); padding: 14px; border-radius: 8px; margin-bottom: 16px; }
|
|
.line-edit-stock { font-size: 14px; color: var(--colortextmuted); }
|
|
.line-edit-stock strong { color: var(--colortext); }
|
|
.line-edit-qty { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
|
.line-edit-qty label { font-size: 15px; font-weight: 500; color: var(--colortext); }
|
|
.line-edit-qty input { width: 90px; padding: 10px; text-align: center; font-size: 18px; font-weight: 600; border: 1px solid var(--colorborder); border-radius: 6px; background: var(--colorbackinput); color: var(--colortext); }
|
|
.line-edit-buttons { display: flex; gap: 10px; }
|
|
.line-edit-buttons .action-btn { flex: 1; margin-top: 0; padding: 14px; }
|
|
|
|
.no-orders { text-align: center; padding: 30px; color: var(--colortextmuted); }
|
|
.main-content { /* Wrapper für den normalen Content */ }
|
|
|
|
/* Orders Modal (for toolbar button) */
|
|
.orders-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.85); display: flex; align-items: flex-start; justify-content: center; z-index: 10000; padding: 15px; padding-top: 40px; }
|
|
.orders-modal-content { background: var(--colorbackbody); border-radius: 12px; width: 100%; max-width: 450px; max-height: 85vh; display: flex; flex-direction: column; border: 1px solid var(--colorborder); overflow: hidden; }
|
|
.orders-modal .orders-panel-header { padding: 14px 16px; background: var(--colorbacktitle); border-bottom: 1px solid var(--colorborder); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
|
|
.orders-modal .orders-panel-title { font-weight: 600; font-size: 17px; color: var(--colortext); }
|
|
.orders-modal .orders-scroller { flex: 1; overflow-x: auto; overflow-y: auto; padding: 15px; -webkit-overflow-scrolling: touch; }
|
|
.orders-modal .order-card { display: block; width: 100%; padding: 14px; margin-bottom: 12px; background: var(--colorbackline); border: 2px solid var(--colorborder); border-radius: 10px; cursor: pointer; }
|
|
.orders-modal .order-card.active { border-color: var(--butactionbg); background: rgba(173,140,79,0.15); }
|
|
.orders-modal .order-card.direkt { font-weight: bold; }
|
|
.orders-modal .order-card.direkt .order-card-ref { font-weight: 700; }
|
|
.orders-modal .order-card-supplier { font-size: 16px; font-weight: 600; margin-bottom: 6px; color: var(--colortext); }
|
|
.orders-modal .order-card-ref { font-size: 13px; color: var(--colortextmuted); margin-bottom: 6px; }
|
|
.orders-modal .order-card-status { display: inline-block; font-size: 11px; padding: 3px 8px; border-radius: 4px; background: var(--colorbacktitle); color: var(--colortextmuted); }
|
|
.orders-modal .order-card-status.draft { background: rgba(188,149,38,0.3); color: var(--warning); }
|
|
.orders-modal .order-card-status.validated { background: rgba(37,165,128,0.3); color: var(--success); }
|
|
.orders-modal .order-card-info { margin-top: 10px; font-size: 12px; color: var(--colortextmuted); }
|
|
.orders-modal .order-detail-section { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
.orders-modal .order-detail-header { padding: 14px 16px; background: var(--colorbacktitle); border-bottom: 1px solid var(--colorborder); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
|
|
.orders-modal .order-detail-title { font-weight: 600; font-size: 15px; color: var(--colortext); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
.orders-modal .order-detail-content { flex: 1; overflow-y: auto; padding: 15px; }
|
|
.orders-modal .loading-spinner { width: 40px; height: 40px; border: 3px solid var(--colorborder); border-top-color: var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin: 30px auto; }
|
|
|
|
/* Print barcode button in product card */
|
|
.product-card-header { display: flex; align-items: flex-start; gap: 10px; }
|
|
.product-card-header .product-info { flex: 1; }
|
|
.print-barcode-btn { width: 44px; height: 44px; border-radius: 8px; border: 1px solid var(--colorborder); background: var(--colorbacktitle); color: var(--colortext); cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
.print-barcode-btn:active { transform: scale(0.95); opacity: 0.8; }
|
|
.print-barcode-btn svg { width: 22px; height: 22px; }
|
|
|
|
/* Barcode Print Modal */
|
|
.print-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.85); display: flex; align-items: center; justify-content: center; z-index: 10000; padding: 15px; }
|
|
.print-modal-content { background: #fff; border-radius: 12px; width: 100%; max-width: 350px; padding: 20px; text-align: center; }
|
|
.print-modal-title { font-size: 16px; font-weight: 600; color: #333; margin-bottom: 15px; }
|
|
.print-modal-barcode { background: #fff; padding: 15px; margin-bottom: 15px; }
|
|
.print-modal-barcode svg { max-width: 100%; height: auto; }
|
|
.print-modal-ref { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 5px; }
|
|
.print-modal-label { font-size: 12px; color: #666; margin-bottom: 15px; }
|
|
.print-modal-buttons { display: flex; gap: 8px; }
|
|
.print-modal-buttons .action-btn { flex: 1; margin: 0; padding: 10px 12px; font-size: 14px; }
|
|
/* Print-specific styles */
|
|
@media print {
|
|
body * { visibility: hidden; }
|
|
.print-area, .print-area * { visibility: visible; }
|
|
.print-area { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); background: #fff; padding: 5mm; }
|
|
.print-barcode-container { width: 24mm; text-align: center; }
|
|
.print-barcode-container svg { width: 100%; height: auto; }
|
|
.print-barcode-ref { font-size: 8pt; font-weight: bold; margin-top: 2mm; font-family: Arial, sans-serif; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Loading Overlay -->
|
|
<div id="loading-overlay" class="loading-overlay">
|
|
<div class="loading-spinner"></div>
|
|
</div>
|
|
|
|
<!-- Login Screen -->
|
|
<div id="login-screen" class="login-screen hidden">
|
|
<div class="login-box">
|
|
<div class="login-logo">
|
|
<svg viewBox="0 0 24 24"><path d="M2 6h2v12H2V6zm3 0h1v12H5V6zm2 0h2v12H7V6zm3 0h1v12h-1V6zm2 0h3v12h-3V6zm4 0h1v12h-1V6zm2 0h1v12h-1V6zm2 0h2v12h-2V6z"/></svg>
|
|
</div>
|
|
<h1 class="login-title">Barcode Scanner</h1>
|
|
<p class="login-subtitle">Anmelden um fortzufahren</p>
|
|
|
|
<div id="login-error" class="login-error"></div>
|
|
|
|
<form id="login-form" class="login-form">
|
|
<div class="form-group">
|
|
<label class="form-label">Benutzername</label>
|
|
<input type="text" id="login-user" class="form-input" autocomplete="username" autocapitalize="none" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Passwort</label>
|
|
<input type="password" id="login-pass" class="form-input" autocomplete="current-password" required>
|
|
</div>
|
|
<div class="remember-row">
|
|
<input type="checkbox" id="login-remember" checked>
|
|
<label for="login-remember">Angemeldet bleiben (15 Tage)</label>
|
|
</div>
|
|
<button type="submit" id="login-btn" class="login-btn">Anmelden</button>
|
|
</form>
|
|
|
|
<div class="login-info">
|
|
Verwende deine Dolibarr-Zugangsdaten
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scanner App -->
|
|
<div id="scanner-app" class="app hidden">
|
|
<!-- Header -->
|
|
<header class="header">
|
|
<span class="header-title">Barcode Scanner</span>
|
|
<div class="header-user">
|
|
<span id="header-username" class="header-username"></span>
|
|
<button type="button" id="logout-btn" class="header-logout">Abmelden</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Mode Tabs (kein Seitenreload, Kamera bleibt aktiv) -->
|
|
<nav class="mode-tabs">
|
|
<?php if ($enableOrder): ?>
|
|
<button type="button" class="mode-tab scanner-tab <?php echo $mode === 'order' ? 'active' : ''; ?>" data-mode="order" onclick="scannerSwitchMode('order', this)">Bestellen</button>
|
|
<?php endif; ?>
|
|
<?php if ($enableShop): ?>
|
|
<button type="button" class="mode-tab scanner-tab <?php echo $mode === 'shop' ? 'active' : ''; ?>" data-mode="shop" onclick="scannerSwitchMode('shop', this)">Shop</button>
|
|
<?php endif; ?>
|
|
<?php if ($enableInventory): ?>
|
|
<button type="button" class="mode-tab scanner-tab <?php echo $mode === 'inventory' ? 'active' : ''; ?>" data-mode="inventory" onclick="scannerSwitchMode('inventory', this)">Inventur</button>
|
|
<?php endif; ?>
|
|
</nav>
|
|
<script>
|
|
function scannerSwitchMode(mode, btn) {
|
|
document.querySelectorAll('.scanner-tab').forEach(function(t) { t.classList.remove('active'); });
|
|
btn.classList.add('active');
|
|
if (typeof SCANNER_CONFIG !== 'undefined' && SCANNER_CONFIG) SCANNER_CONFIG.mode = mode;
|
|
if (typeof window.SCANNER_CONFIG !== 'undefined' && window.SCANNER_CONFIG) window.SCANNER_CONFIG.mode = mode;
|
|
history.replaceState(null, '', window.location.pathname + '?mode=' + mode);
|
|
// Order-Mode Buttons ein/ausblenden
|
|
var searchBtn = document.getElementById('ctrl-search');
|
|
var freetextBtn = document.getElementById('ctrl-freetext');
|
|
var swipeHint = document.getElementById('swipe-hint');
|
|
if (searchBtn) searchBtn.style.display = (mode === 'order') ? 'flex' : 'none';
|
|
if (freetextBtn) freetextBtn.style.display = (mode === 'order') ? 'flex' : 'none';
|
|
if (swipeHint) swipeHint.style.display = (mode === 'order') ? 'block' : 'none';
|
|
if (typeof window._scannerHideResult === 'function') window._scannerHideResult();
|
|
if (window._scannerCurrentProduct && typeof window._scannerShowResult === 'function') {
|
|
window._scannerShowResult(window._scannerCurrentProduct);
|
|
}
|
|
}
|
|
// Initial Buttons anzeigen wenn order mode
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var mode = '<?php echo $mode; ?>';
|
|
var searchBtn = document.getElementById('ctrl-search');
|
|
var freetextBtn = document.getElementById('ctrl-freetext');
|
|
var swipeHint = document.getElementById('swipe-hint');
|
|
if (mode === 'order') {
|
|
if (searchBtn) searchBtn.style.display = 'flex';
|
|
if (freetextBtn) freetextBtn.style.display = 'flex';
|
|
if (swipeHint) swipeHint.style.display = 'block';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<!-- Scanner Area -->
|
|
<main class="scanner-area">
|
|
<div class="scanner-box">
|
|
<div id="scanner-video-container" class="scanner-video-box">
|
|
<video id="scanner-video" playsinline></video>
|
|
<div class="scan-region-highlight"></div>
|
|
</div>
|
|
<div class="scanner-controls" id="scanner-controls">
|
|
<!-- Search button (left) - only in order mode -->
|
|
<button type="button" class="scan-tool-btn search-btn" id="ctrl-search" style="display:none;" title="Produktsuche">
|
|
<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>
|
|
</button>
|
|
<!-- Barcode Type Selector -->
|
|
<div class="barcode-type-selector">
|
|
<select id="barcode-type-select" class="barcode-type-dropdown">
|
|
<option value="CODE_128">Code 128</option>
|
|
<option value="QR_CODE">QR-Code</option>
|
|
<option value="DATA_MATRIX">DataMatrix</option>
|
|
<option value="OCR">OCR</option>
|
|
</select>
|
|
</div>
|
|
<!-- OCR Digit Count Selector (nur sichtbar bei OCR-Modus) -->
|
|
<div class="ocr-digit-selector hidden" id="ocr-digit-selector">
|
|
<select id="ocr-digit-count" class="barcode-type-dropdown ocr-digit-dropdown">
|
|
<option value="5">5</option>
|
|
<option value="6">6</option>
|
|
<option value="7">7</option>
|
|
<option value="8" selected>8</option>
|
|
<option value="9">9</option>
|
|
<option value="10">10</option>
|
|
<option value="11">11</option>
|
|
<option value="12">12</option>
|
|
</select>
|
|
</div>
|
|
<!-- Scan buttons (center) -->
|
|
<div class="scan-buttons-group">
|
|
<button type="button" id="start-scan-btn" class="scan-btn start">Scannen</button>
|
|
<button type="button" id="stop-scan-btn" class="scan-btn stop hidden">Stopp</button>
|
|
</div>
|
|
<!-- Freetext button (right) - only in order mode -->
|
|
<button type="button" class="scan-tool-btn freetext-btn" id="ctrl-freetext" style="display:none;" title="Freitext">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
|
|
</button>
|
|
<!-- History button -->
|
|
<button type="button" class="scan-tool-btn history-btn" id="ctrl-history" title="Scan-Historie">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<polyline points="12 6 12 12 16 14"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="scanner-last-scan">
|
|
<span class="last-scan-label">Letzter Scan:</span>
|
|
<span id="last-scan-code">-</span>
|
|
</div>
|
|
|
|
|
|
<div id="result-area" class="scanner-result hidden"></div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
// PWA Auth Manager
|
|
const PWA_AUTH = {
|
|
STORAGE_KEY: 'hbs_pwa_auth',
|
|
ajaxUrl: '<?php echo dol_buildpath('/handybarcodescanner/ajax/', 1); ?>',
|
|
|
|
// Gespeicherte Auth-Daten laden
|
|
getStoredAuth: function() {
|
|
try {
|
|
const data = localStorage.getItem(this.STORAGE_KEY);
|
|
if (!data) return null;
|
|
const auth = JSON.parse(data);
|
|
// Ablauf pruefen
|
|
if (auth.expires && auth.expires < Date.now() / 1000) {
|
|
this.clearAuth();
|
|
return null;
|
|
}
|
|
return auth;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
// Auth-Daten speichern
|
|
saveAuth: function(data) {
|
|
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
|
|
},
|
|
|
|
// Auth-Daten loeschen
|
|
clearAuth: function() {
|
|
localStorage.removeItem(this.STORAGE_KEY);
|
|
},
|
|
|
|
// Login durchfuehren
|
|
login: function(username, password, remember) {
|
|
return fetch(this.ajaxUrl + 'pwa_login.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: 'login=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password)
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success && remember) {
|
|
this.saveAuth({
|
|
pwa_token: data.pwa_token,
|
|
csrf_token: data.csrf_token,
|
|
user: data.user,
|
|
expires: data.expires
|
|
});
|
|
}
|
|
return data;
|
|
});
|
|
},
|
|
|
|
// Token verifizieren und Session erneuern
|
|
verify: function() {
|
|
const auth = this.getStoredAuth();
|
|
if (!auth || !auth.pwa_token) {
|
|
return Promise.resolve({ success: false, need_login: true });
|
|
}
|
|
|
|
return fetch(this.ajaxUrl + 'pwa_verify.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-PWA-Token': auth.pwa_token
|
|
},
|
|
body: 'pwa_token=' + encodeURIComponent(auth.pwa_token)
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// CSRF-Token aktualisieren
|
|
auth.csrf_token = data.csrf_token;
|
|
this.saveAuth(auth);
|
|
}
|
|
return data;
|
|
});
|
|
},
|
|
|
|
// Logout
|
|
logout: function() {
|
|
this.clearAuth();
|
|
location.reload();
|
|
}
|
|
};
|
|
|
|
// UI Elements
|
|
const loadingOverlay = document.getElementById('loading-overlay');
|
|
const loginScreen = document.getElementById('login-screen');
|
|
const scannerApp = document.getElementById('scanner-app');
|
|
const loginForm = document.getElementById('login-form');
|
|
const loginError = document.getElementById('login-error');
|
|
const loginBtn = document.getElementById('login-btn');
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|
const headerUsername = document.getElementById('header-username');
|
|
|
|
// Scanner Config - wird nach Login gesetzt
|
|
var SCANNER_CONFIG = null;
|
|
|
|
// App initialisieren
|
|
async function initApp() {
|
|
// Gespeicherte Auth pruefen
|
|
const auth = PWA_AUTH.getStoredAuth();
|
|
|
|
if (auth && auth.pwa_token) {
|
|
// Token verifizieren
|
|
try {
|
|
const result = await PWA_AUTH.verify();
|
|
if (result.success) {
|
|
showScanner(auth, result.csrf_token);
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.error('Verify error:', e);
|
|
}
|
|
}
|
|
|
|
// Login anzeigen
|
|
showLogin();
|
|
}
|
|
|
|
function showLogin() {
|
|
loadingOverlay.classList.add('hidden');
|
|
loginScreen.classList.remove('hidden');
|
|
scannerApp.classList.add('hidden');
|
|
document.getElementById('login-user').focus();
|
|
}
|
|
|
|
function showScanner(auth, csrfToken) {
|
|
loadingOverlay.classList.add('hidden');
|
|
loginScreen.classList.add('hidden');
|
|
scannerApp.classList.remove('hidden');
|
|
|
|
// Username anzeigen
|
|
const user = auth.user || {};
|
|
headerUsername.textContent = user.firstname ? (user.firstname + ' ' + user.lastname) : user.login;
|
|
|
|
// Scanner Config setzen
|
|
SCANNER_CONFIG = {
|
|
ajaxUrl: PWA_AUTH.ajaxUrl,
|
|
token: csrfToken || auth.csrf_token,
|
|
mode: '<?php echo $mode; ?>',
|
|
enableVibration: <?php echo getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_VIBRATION', 1); ?>,
|
|
enableSound: <?php echo getDolGlobalInt('HANDYBARCODESCANNER_ENABLE_SOUND', 0); ?>,
|
|
lang: {
|
|
productNotFound: 'Produkt nicht gefunden',
|
|
added: 'Produkt eingestellt',
|
|
saved: 'Gespeichert',
|
|
error: 'Fehler',
|
|
selectSupplier: 'Lieferant waehlen',
|
|
noSupplier: 'Kein Lieferant fuer dieses Produkt hinterlegt',
|
|
quantity: 'Menge',
|
|
add: 'Hinzufuegen',
|
|
price: 'Preis',
|
|
stock: 'Lagerbestand',
|
|
currentStock: 'Aktueller Bestand',
|
|
newStock: 'Neuer Bestand',
|
|
save: 'Speichern',
|
|
confirmStockChange: 'Bestandsaenderung bestaetigen',
|
|
cancel: 'Abbrechen',
|
|
confirm: 'Bestaetigen',
|
|
openShop: 'Shop oeffnen',
|
|
cheapest: 'Guenstigster',
|
|
cameraError: 'Kamera-Zugriff fehlgeschlagen',
|
|
ref: 'Referenz',
|
|
product: 'Produkt',
|
|
supplierOrderRef: 'Lieferantenbestellnummer',
|
|
startScan: 'Scannen starten',
|
|
stopScan: 'Scannen stoppen'
|
|
}
|
|
};
|
|
|
|
// Scanner.js Config global setzen
|
|
window.SCANNER_CONFIG = SCANNER_CONFIG;
|
|
|
|
// Scanner.js initialisieren (mit kleiner Verzoegerung fuer DOM)
|
|
setTimeout(function() {
|
|
if (typeof window.initScanner === 'function') {
|
|
window.initScanner();
|
|
} else {
|
|
console.error('initScanner not found');
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
// Login Form Handler
|
|
loginForm.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const username = document.getElementById('login-user').value.trim();
|
|
const password = document.getElementById('login-pass').value;
|
|
const remember = document.getElementById('login-remember').checked;
|
|
|
|
if (!username || !password) {
|
|
loginError.textContent = 'Benutzername und Passwort erforderlich';
|
|
loginError.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
loginBtn.disabled = true;
|
|
loginBtn.textContent = 'Anmelden...';
|
|
loginError.style.display = 'none';
|
|
|
|
try {
|
|
const result = await PWA_AUTH.login(username, password, remember);
|
|
|
|
if (result.success) {
|
|
showScanner({
|
|
pwa_token: result.pwa_token,
|
|
csrf_token: result.csrf_token,
|
|
user: result.user,
|
|
expires: result.expires
|
|
}, result.csrf_token);
|
|
} else {
|
|
loginError.textContent = result.error || 'Login fehlgeschlagen';
|
|
loginError.style.display = 'block';
|
|
loginBtn.disabled = false;
|
|
loginBtn.textContent = 'Anmelden';
|
|
}
|
|
} catch (e) {
|
|
console.error('Login error:', e);
|
|
loginError.textContent = 'Verbindungsfehler';
|
|
loginError.style.display = 'block';
|
|
loginBtn.disabled = false;
|
|
loginBtn.textContent = 'Anmelden';
|
|
}
|
|
});
|
|
|
|
// Logout Handler
|
|
logoutBtn.addEventListener('click', function() {
|
|
PWA_AUTH.logout();
|
|
});
|
|
|
|
// Control Buttons Handler (neben Scan-Button)
|
|
document.getElementById('ctrl-search').addEventListener('click', function() {
|
|
if (typeof window._showProductSearchModal === 'function') {
|
|
window._showProductSearchModal();
|
|
}
|
|
});
|
|
document.getElementById('ctrl-freetext').addEventListener('click', function() {
|
|
if (typeof window._showFreetextModal === 'function') {
|
|
window._showFreetextModal();
|
|
}
|
|
});
|
|
|
|
// Global Swipe für Bestellungen und Barcode-Druck
|
|
(function() {
|
|
let startX = 0;
|
|
let startY = 0;
|
|
const scannerArea = document.querySelector('.scanner-area');
|
|
if (!scannerArea) return;
|
|
|
|
scannerArea.addEventListener('touchstart', function(e) {
|
|
startX = e.touches[0].clientX;
|
|
startY = e.touches[0].clientY;
|
|
}, { passive: true });
|
|
|
|
scannerArea.addEventListener('touchend', function(e) {
|
|
const endX = e.changedTouches[0].clientX;
|
|
const endY = e.changedTouches[0].clientY;
|
|
const deltaX = endX - startX;
|
|
const deltaY = Math.abs(endY - startY);
|
|
|
|
// Swipe left (mindestens 60px, mehr horizontal als vertikal) -> Bestellungen
|
|
if (deltaX < -60 && deltaY < 100) {
|
|
if (window.SCANNER_CONFIG && window.SCANNER_CONFIG.mode === 'order') {
|
|
if (typeof window._showOrdersPanel === 'function') {
|
|
window._showOrdersPanel();
|
|
}
|
|
}
|
|
}
|
|
// Swipe right -> Barcode drucken (Produktsuche öffnen)
|
|
if (deltaX > 60 && deltaY < 100) {
|
|
showPrintSearchModal();
|
|
}
|
|
}, { passive: true });
|
|
})();
|
|
|
|
// Produktsuche für Barcode-Druck
|
|
function showPrintSearchModal() {
|
|
// Scanner pausieren
|
|
if (typeof window._pauseScanner === 'function') {
|
|
window._pauseScanner();
|
|
}
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'search-modal';
|
|
modal.id = 'print-search-modal';
|
|
modal.innerHTML = `
|
|
<div class="search-modal-content">
|
|
<div class="search-modal-header">
|
|
<input type="text" id="print-search-input" placeholder="Produkt für Barcode-Druck suchen..." autofocus>
|
|
<button type="button" class="search-close-btn" id="print-search-close">×</button>
|
|
</div>
|
|
<div class="search-results" id="print-search-results"></div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
|
|
const input = document.getElementById('print-search-input');
|
|
const results = document.getElementById('print-search-results');
|
|
let searchTimeout = null;
|
|
|
|
function closeModal() {
|
|
modal.remove();
|
|
if (typeof window._resumeScanner === 'function') {
|
|
window._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(() => {
|
|
searchProductsForPrint(query, closeModal);
|
|
}, 300);
|
|
});
|
|
|
|
document.getElementById('print-search-close').addEventListener('click', closeModal);
|
|
|
|
modal.addEventListener('click', function(e) {
|
|
if (e.target === modal) closeModal();
|
|
});
|
|
|
|
input.focus();
|
|
}
|
|
|
|
function searchProductsForPrint(query, closeModalCallback) {
|
|
const token = window.SCANNER_CONFIG ? window.SCANNER_CONFIG.token : '';
|
|
const ajaxUrl = window.SCANNER_CONFIG ? window.SCANNER_CONFIG.ajaxUrl : 'ajax/';
|
|
|
|
fetch(ajaxUrl + 'searchproduct.php?token=' + token + '&q=' + encodeURIComponent(query), {credentials: 'same-origin'})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const results = document.getElementById('print-search-results');
|
|
if (!results) return;
|
|
|
|
if (data.success && data.products && 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">${escapeHtmlPwa(p.label)}</div>
|
|
<div class="search-result-info">
|
|
<span>Ref: ${escapeHtmlPwa(p.ref)}</span>
|
|
<span>${p.barcode ? 'Barcode: ' + escapeHtmlPwa(p.barcode) : 'Kein Barcode'}</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();
|
|
if (product.barcode) {
|
|
showPrintBarcodeModal(product);
|
|
} else {
|
|
alert('Dieses Produkt hat keinen Barcode');
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
results.innerHTML = '<div class="search-loading">Keine Produkte gefunden</div>';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error('Search error:', err);
|
|
const results = document.getElementById('print-search-results');
|
|
if (results) {
|
|
results.innerHTML = '<div class="search-loading">Fehler bei der Suche</div>';
|
|
}
|
|
});
|
|
}
|
|
|
|
function escapeHtmlPwa(str) {
|
|
if (!str) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Barcode Print Modal
|
|
function showPrintBarcodeModal(product) {
|
|
if (!product || !product.barcode) {
|
|
alert('Kein Barcode vorhanden');
|
|
return;
|
|
}
|
|
|
|
// Scanner pausieren
|
|
if (typeof window._pauseScanner === 'function') {
|
|
window._pauseScanner();
|
|
}
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'print-modal';
|
|
modal.id = 'print-modal';
|
|
modal.innerHTML = `
|
|
<div class="print-modal-content">
|
|
<div class="print-modal-title">Barcode drucken</div>
|
|
<div class="print-modal-barcode">
|
|
<svg id="print-barcode-svg"></svg>
|
|
</div>
|
|
<div class="print-modal-ref">${product.ref || ''}</div>
|
|
<div class="print-modal-label">${product.label || ''}</div>
|
|
<div class="print-modal-buttons">
|
|
<button type="button" class="action-btn btn-secondary" id="print-cancel">Schließen</button>
|
|
<button type="button" class="action-btn btn-primary" id="print-brother" style="background:#00796b;">
|
|
<svg style="width:18px;height:18px;margin-right:6px;vertical-align:middle;" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M18 3H6C4.9 3 4 3.9 4 5v14c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-1 9h-4v4h-2v-4H7v-2h4V6h2v4h4v2z"/>
|
|
</svg>
|
|
Brother
|
|
</button>
|
|
<button type="button" class="action-btn btn-primary" id="print-now">Browser</button>
|
|
</div>
|
|
</div>
|
|
<!-- Hidden print area -->
|
|
<div class="print-area" id="print-area" style="position:fixed;left:-9999px;">
|
|
<div class="print-barcode-container">
|
|
<svg id="print-barcode-svg-hidden"></svg>
|
|
<div class="print-barcode-ref">${product.ref || ''}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
|
|
function closeModal() {
|
|
modal.remove();
|
|
// Scanner fortsetzen
|
|
if (typeof window._resumeScanner === 'function') {
|
|
window._resumeScanner();
|
|
}
|
|
}
|
|
|
|
// Generate barcode with JsBarcode
|
|
try {
|
|
JsBarcode('#print-barcode-svg', product.barcode, {
|
|
format: 'CODE128',
|
|
width: 2,
|
|
height: 60,
|
|
displayValue: true,
|
|
fontSize: 14,
|
|
margin: 5
|
|
});
|
|
// Also generate for print area (smaller for 24mm label)
|
|
JsBarcode('#print-barcode-svg-hidden', product.barcode, {
|
|
format: 'CODE128',
|
|
width: 1.5,
|
|
height: 40,
|
|
displayValue: true,
|
|
fontSize: 10,
|
|
margin: 2
|
|
});
|
|
} catch (e) {
|
|
console.error('Barcode generation error:', e);
|
|
document.querySelector('.print-modal-barcode').innerHTML = '<div style="color:red;">Barcode-Fehler: ' + e.message + '</div>';
|
|
}
|
|
|
|
// Close button
|
|
document.getElementById('print-cancel').addEventListener('click', closeModal);
|
|
|
|
// Brother Print button
|
|
document.getElementById('print-brother').addEventListener('click', function() {
|
|
const intentUrl = `brotherprint://print?barcode=${encodeURIComponent(product.barcode)}&ref=${encodeURIComponent(product.ref || '')}`;
|
|
console.log('Brother Print Intent:', intentUrl);
|
|
window.location.href = intentUrl;
|
|
// Modal nach kurzem Delay schließen (App öffnet sich)
|
|
setTimeout(closeModal, 500);
|
|
});
|
|
|
|
// Browser Print button
|
|
document.getElementById('print-now').addEventListener('click', function() {
|
|
const printArea = document.getElementById('print-area');
|
|
printArea.style.left = '0';
|
|
printArea.style.top = '0';
|
|
printArea.style.position = 'fixed';
|
|
printArea.style.zIndex = '99999';
|
|
window.print();
|
|
printArea.style.left = '-9999px';
|
|
});
|
|
|
|
// Close on backdrop click
|
|
modal.addEventListener('click', function(e) {
|
|
if (e.target === modal) closeModal();
|
|
});
|
|
}
|
|
// Global export for scanner.js
|
|
window._showPrintBarcodeModal = showPrintBarcodeModal;
|
|
|
|
// Service Worker registrieren
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.register('sw.js').catch(function(err) {
|
|
console.log('ServiceWorker registration failed:', err);
|
|
});
|
|
}
|
|
|
|
// App starten
|
|
initApp();
|
|
</script>
|
|
<script src="https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/umd/index.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@5.1.0/dist/tesseract.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=91"></script>
|
|
</body>
|
|
</html>
|