v5.0: Bestellmodus erweitert, Barcode-Druck, Scanner-Pausierung

Bestellmodus:
- Produktsuche (Lupe-Button) und Freitext-Positionen (Plus-Button)
- Bestellübersicht per Swipe nach links
- Horizontale Bestellliste mit Direkt-Bestellungen hervorgehoben
- Bestellzeilen bearbeiten (Menge ändern, löschen)
- Auto-Öffnung der zuletzt bearbeiteten Bestellung

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

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-23 09:46:06 +01:00
parent 48bf9411dc
commit 74728a71d1
14 changed files with 2654 additions and 36 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
bin/

View file

@ -1,5 +1,33 @@
# CHANGELOG MODULE HANDYBARCODESCANNER FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
## 5.0
### Bestellmodus - Neue Features
- **Produktsuche**: Lupe-Button neben Scan-Button für manuelle Produktsuche
- **Freitext-Positionen**: Plus-Button für freie Artikelbeschreibungen ohne Produktstamm
- **Bestellübersicht per Swipe**: Nach links swipen öffnet Bestellungen-Modal
- **Horizontale Bestellliste**: Alle offenen Bestellungen horizontal scrollbar
- **Direkt-Bestellungen hervorgehoben**: Bestellungen mit "-Direkt" Suffix fett dargestellt
- **Auto-Öffnung**: Zuletzt bearbeitete Bestellung wird automatisch geöffnet
- **Bestellzeilen bearbeiten**: Produktname + Menge in Zeilen, Klick öffnet Bearbeitungsdialog
- **Zeilen-Dialog**: Lagerbestand anzeigen, Menge ändern, Position löschen
### Barcode-Druck
- **Rechts-Swipen**: Öffnet Produktsuche für Barcode-Druck
- **Print-Modal**: Zeigt Code128-Barcode mit Produktreferenz
- **24mm Label-Format**: Optimiert für Brother P-touch und ähnliche Etikettendrucker
- **JsBarcode Integration**: Barcode-Generierung direkt im Browser
### Scanner-Verbesserungen
- **Dialog-Pausierung**: Scanner pausiert automatisch bei geöffneten Dialogen
- **Auto-Fortsetzung**: Scanner läuft weiter wenn alle Dialoge geschlossen
- **Stopp-Button Fix**: Zuverlässiges Stoppen des Scanners jederzeit möglich
### UI/UX
- **Kompakterer Scan-Button**: Mehr Platz für Tool-Buttons
- **Swipe-Hinweis**: Visueller Hinweis "← Bestellungen" im Order-Mode
- **Service Worker v5.3**: Aktualisiertes Caching mit JsBarcode-Library
## 4.7
- PWA-Link auf der Scanner-Seite angezeigt (korrekter externer Hostname statt interner IP)

View file

@ -7,12 +7,15 @@ Mobiler Barcode-Scanner für Dolibarr - optimiert für die Verwendung auf Smartp
Das Modul bietet drei Modi für die mobile Barcode-Erfassung:
### 1. Bestellmodus (Order)
- Produkt per Barcode scannen
- Produkt per Barcode scannen oder manuell suchen (Lupe-Button)
- Alle verfügbaren Lieferanten mit Einkaufspreisen werden angezeigt
- Günstigster Lieferant ist vorausgewählt
- Produkt wird zu einer lieferantenspezifischen Entwurfsbestellung hinzugefügt
- Bestellungen werden automatisch als "Direktbestellung-[Lieferantenname]" erstellt
- Bestellungen werden automatisch als "YYMMDD-Direkt" erstellt (z.B. "260223-Direkt")
- Falls kein Lieferant zugewiesen: Manuelle Auswahl aller verfügbaren Lieferanten
- **Freitext-Positionen**: Plus-Button für Artikel ohne Produktstamm
- **Bestellübersicht**: Nach links swipen zeigt alle offenen Bestellungen
- **Zeilen bearbeiten**: Klick auf Bestellzeile öffnet Dialog zum Ändern/Löschen
### 2. Shop-Modus
- Produkt per Barcode scannen
@ -25,6 +28,12 @@ Das Modul bietet drei Modi für die mobile Barcode-Erfassung:
- Neuen Bestand eingeben und mit Bestätigungsdialog speichern
- Lagerbewegungen werden korrekt protokolliert
### 4. Barcode-Druck
- Nach rechts swipen öffnet Produktsuche für Barcode-Druck
- Produkt auswählen → Barcode-Vorschau wird angezeigt
- Code128-Format, optimiert für 24mm Etikettendrucker (Brother P-touch etc.)
- Drucken direkt vom Smartphone über Browser-Druckfunktion
## Barcode-Unterstützung
Das Modul sucht Barcodes in folgender Reihenfolge:
@ -104,9 +113,13 @@ Die Modi (Bestellen/Shop/Inventur) können per Tab gewechselt werden, ohne dass
### Im mobilen Browser / PWA
1. QR-Code von der Admin-Seite scannen oder URL direkt eingeben
2. Kamerazugriff erlauben (HTTPS erforderlich!)
3. Gewünschten Modus wählen und "Scan starten" tippen
3. Gewünschten Modus wählen und "Scannen" tippen
4. Barcode vor die Kamera halten
### Gesten-Steuerung
- **Nach links swipen**: Bestellübersicht öffnen (Order-Mode)
- **Nach rechts swipen**: Barcode-Druck (Produktsuche öffnet sich)
## Technische Details
### Dateistruktur
@ -137,6 +150,7 @@ handybarcodescanner/
### Verwendete Bibliotheken
- [QuaggaJS](https://github.com/ericblade/quagga2) - Browser-basierte Barcode-Erkennung
- [JsBarcode](https://github.com/lindell/JsBarcode) - Barcode-Generierung für Druck
## Changelog

144
ajax/addfreetextline.php Normal file
View file

@ -0,0 +1,144 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* AJAX: Add free text line to supplier order
*/
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1');
}
if (!defined('NOREQUIREMENU')) {
define('NOREQUIREMENU', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
if (!defined('NOREQUIREAJAX')) {
define('NOREQUIREAJAX', '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(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr']));
}
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
header('Content-Type: application/json; charset=utf-8');
// Security check
if (!$user->hasRight('fournisseur', 'commande', 'creer') && !$user->hasRight('supplier_order', 'creer')) {
echo json_encode(['success' => false, 'error' => 'Access denied']);
exit;
}
$supplierId = GETPOSTINT('supplier_id');
$description = GETPOST('description', 'alphanohtml');
$qty = GETPOSTFLOAT('qty');
$price = GETPOSTFLOAT('price');
if (empty($supplierId) || empty($description) || $qty <= 0) {
echo json_encode(['success' => false, 'error' => 'Missing parameters (supplier_id, description, qty required)']);
exit;
}
// Load supplier
$supplier = new Societe($db);
if ($supplier->fetch($supplierId) <= 0) {
echo json_encode(['success' => false, 'error' => 'Supplier not found']);
exit;
}
// Build ref_supplier for today
$refSupplier = date('ymd') . '-Direkt';
// Search for existing draft order for this supplier with today's ref_supplier
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."commande_fournisseur";
$sql .= " WHERE fk_soc = ".((int) $supplierId);
$sql .= " AND fk_statut = 0"; // Draft status
$sql .= " AND ref_supplier = '".$db->escape($refSupplier)."'";
$sql .= " AND entity IN (".getEntity('supplier_order').")";
$sql .= " ORDER BY rowid DESC LIMIT 1";
$resql = $db->query($sql);
$existingOrderId = 0;
if ($resql && $db->num_rows($resql) > 0) {
$obj = $db->fetch_object($resql);
$existingOrderId = $obj->rowid;
}
$order = new CommandeFournisseur($db);
if ($existingOrderId > 0) {
$order->fetch($existingOrderId);
} else {
// Create new order
$order->socid = $supplierId;
$order->ref_supplier = $refSupplier;
$order->cond_reglement_id = $supplier->cond_reglement_supplier_id ?: getDolGlobalInt('FOURN_COND_REGLEMENT_ID_DEFAULT', 1);
$order->mode_reglement_id = $supplier->mode_reglement_supplier_id ?: getDolGlobalInt('FOURN_MODE_REGLEMENT_ID_DEFAULT', 1);
$order->date = dol_now();
$order->date_livraison = dol_now() + (7 * 24 * 60 * 60);
$result = $order->create($user);
if ($result < 0) {
echo json_encode(['success' => false, 'error' => 'Failed to create order: ' . $order->error]);
exit;
}
}
// Add free text line (no product_id)
$tva_tx = getDolGlobalString('MAIN_DEFAULT_TVA') ?: 19;
$result = $order->addline(
$description, // description
$price, // pu_ht (unit price)
$qty, // qty
$tva_tx, // txtva
0, // localtax1
0, // localtax2
0, // fk_product (0 = free text)
0, // fk_prod_fourn_price
'', // ref_fourn
0, // remise_percent
'HT', // price_base_type
0, // pu_devise
0, // type (0 = product, 1 = service)
0, // rang
0 // special_code
);
if ($result < 0) {
echo json_encode(['success' => false, 'error' => 'Failed to add line: ' . $order->error]);
exit;
}
// Get updated order info
$order->fetch($order->id);
$totalItems = count($order->lines);
$totalQty = 0;
foreach ($order->lines as $line) {
$totalQty += $line->qty;
}
echo json_encode([
'success' => true,
'order_id' => $order->id,
'order_ref' => $order->ref,
'total_items' => $totalItems,
'total_qty' => $totalQty,
'message' => 'Free text line added to ' . $order->ref
]);

104
ajax/getorderlines.php Normal file
View file

@ -0,0 +1,104 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* AJAX: Get order lines for a specific supplier order
*/
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1');
}
if (!defined('NOREQUIREMENU')) {
define('NOREQUIREMENU', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
if (!defined('NOREQUIREAJAX')) {
define('NOREQUIREAJAX', '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(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr']));
}
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
header('Content-Type: application/json; charset=utf-8');
// Security check
if (!$user->hasRight('fournisseur', 'commande', 'lire') && !$user->hasRight('supplier_order', 'read')) {
echo json_encode(['success' => false, 'error' => 'Access denied']);
exit;
}
$orderId = GETPOSTINT('order_id');
if (empty($orderId)) {
echo json_encode(['success' => false, 'error' => 'No order_id provided']);
exit;
}
$order = new CommandeFournisseur($db);
if ($order->fetch($orderId) <= 0) {
echo json_encode(['success' => false, 'error' => 'Order not found']);
exit;
}
$lines = [];
foreach ($order->lines as $line) {
$productLabel = $line->product_label ?: $line->desc;
$productRef = $line->product_ref ?: '';
$stock = 0;
// Get stock if product exists
if (!empty($line->fk_product)) {
$product = new Product($db);
if ($product->fetch($line->fk_product) > 0) {
$stock = (float) $product->stock_reel;
if (empty($productLabel)) {
$productLabel = $product->label;
}
if (empty($productRef)) {
$productRef = $product->ref;
}
}
}
$lines[] = [
'id' => (int) $line->id,
'product_id' => (int) $line->fk_product,
'product_ref' => $productRef,
'product_label' => $productLabel,
'qty' => (float) $line->qty,
'price' => (float) $line->subprice,
'total_ht' => (float) $line->total_ht,
'stock' => $stock,
'ref_fourn' => $line->ref_fourn ?: ''
];
}
echo json_encode([
'success' => true,
'order' => [
'id' => $order->id,
'ref' => $order->ref,
'ref_supplier' => $order->ref_supplier,
'status' => $order->statut,
'supplier_name' => $order->thirdparty->name ?? '',
'total_ht' => (float) $order->total_ht
],
'lines' => $lines
]);

111
ajax/getorders.php Normal file
View file

@ -0,0 +1,111 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* AJAX: Get supplier orders for overview
*/
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1');
}
if (!defined('NOREQUIREMENU')) {
define('NOREQUIREMENU', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
if (!defined('NOREQUIREAJAX')) {
define('NOREQUIREAJAX', '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(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr']));
}
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
header('Content-Type: application/json; charset=utf-8');
// Security check
if (!$user->hasRight('fournisseur', 'commande', 'lire') && !$user->hasRight('supplier_order', 'read')) {
echo json_encode(['success' => false, 'error' => 'Access denied']);
exit;
}
$orders = [];
// Get all draft orders (fk_statut = 0) and approved orders (fk_statut = 1)
// Sorted: Direkt-drafts first, then other drafts, then approved
$sql = "SELECT cf.rowid, cf.ref, cf.ref_supplier, cf.fk_statut as status, cf.date_commande, cf.total_ht,";
$sql .= " s.nom as supplier_name, s.rowid as supplier_id,";
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."commande_fournisseurdet WHERE fk_commande = cf.rowid) as line_count,";
$sql .= " (SELECT SUM(qty) FROM ".MAIN_DB_PREFIX."commande_fournisseurdet WHERE fk_commande = cf.rowid) as total_qty";
$sql .= " FROM ".MAIN_DB_PREFIX."commande_fournisseur as cf";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON cf.fk_soc = s.rowid";
$sql .= " WHERE cf.entity IN (".getEntity('supplier_order').")";
$sql .= " AND cf.fk_statut IN (0, 1, 2)"; // Draft, Validated, Approved
$sql .= " ORDER BY";
// Direkt-Bestellungen (Entwurf) zuerst
$sql .= " CASE WHEN cf.fk_statut = 0 AND cf.ref_supplier LIKE '%-Direkt' THEN 0";
// Dann Direkt-Bestellungen (freigestellt)
$sql .= " WHEN cf.fk_statut IN (1,2) AND cf.ref_supplier LIKE '%-Direkt' THEN 1";
// Dann andere Entwürfe
$sql .= " WHEN cf.fk_statut = 0 THEN 2";
// Dann alle anderen
$sql .= " ELSE 3 END,";
$sql .= " cf.date_commande DESC, cf.rowid DESC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$isDirekt = (strpos($obj->ref_supplier, '-Direkt') !== false);
$orders[] = [
'id' => (int) $obj->rowid,
'ref' => $obj->ref,
'ref_supplier' => $obj->ref_supplier,
'status' => (int) $obj->status,
'status_label' => getOrderStatusLabel((int) $obj->status),
'date' => $obj->date_commande,
'total_ht' => (float) $obj->total_ht,
'supplier_id' => (int) $obj->supplier_id,
'supplier_name' => $obj->supplier_name,
'line_count' => (int) $obj->line_count,
'total_qty' => (float) $obj->total_qty,
'is_direkt' => $isDirekt
];
}
}
/**
* Get status label
*/
function getOrderStatusLabel($status) {
switch ($status) {
case 0: return 'Entwurf';
case 1: return 'Validiert';
case 2: return 'Freigegeben';
case 3: return 'Bestellt';
case 4: return 'Teilweise geliefert';
case 5: return 'Geliefert';
case 6: return 'Storniert';
case 7: return 'Annulliert';
case 9: return 'Abgelehnt';
default: return 'Unbekannt';
}
}
echo json_encode([
'success' => true,
'orders' => $orders
]);

121
ajax/searchproduct.php Normal file
View file

@ -0,0 +1,121 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* AJAX: Search products by name/ref for autocomplete
*/
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1');
}
if (!defined('NOREQUIREMENU')) {
define('NOREQUIREMENU', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
if (!defined('NOREQUIREAJAX')) {
define('NOREQUIREAJAX', '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(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr']));
}
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
header('Content-Type: application/json; charset=utf-8');
// Security check
if (!$user->hasRight('produit', 'lire') && !$user->hasRight('product', 'read')) {
echo json_encode(['success' => false, 'error' => 'Access denied']);
exit;
}
$search = GETPOST('q', 'alphanohtml');
$limit = GETPOSTINT('limit') ?: 15;
if (empty($search) || strlen($search) < 2) {
echo json_encode(['success' => true, 'products' => []]);
exit;
}
$products = [];
// Search products by ref, label
$sql = "SELECT p.rowid, p.ref, p.label, p.barcode, p.stock as stock_reel, p.fk_unit";
$sql .= " FROM ".MAIN_DB_PREFIX."product as p";
$sql .= " WHERE p.entity IN (".getEntity('product').")";
$sql .= " AND (p.ref LIKE '%".$db->escape($search)."%'";
$sql .= " OR p.label LIKE '%".$db->escape($search)."%'";
$sql .= " OR p.barcode LIKE '%".$db->escape($search)."%')";
$sql .= " ORDER BY p.label ASC";
$sql .= " LIMIT ".((int) $limit);
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
// Get suppliers for this product
$suppliers = [];
$productFourn = new ProductFournisseur($db);
$supplierPrices = $productFourn->list_product_fournisseur_price($obj->rowid);
if ($supplierPrices) {
foreach ($supplierPrices as $sp) {
// Get supplier name
$supplierSql = "SELECT nom FROM ".MAIN_DB_PREFIX."societe WHERE rowid = ".((int) $sp->fourn_id);
$supplierRes = $db->query($supplierSql);
$supplierObj = $db->fetch_object($supplierRes);
$refFourn = $sp->ref_supplier ?? $sp->fourn_ref ?? $sp->ref_fourn ?? '';
$suppliers[] = [
'id' => $sp->fourn_id,
'name' => $supplierObj->nom ?? 'Unknown',
'price' => (float) $sp->fourn_price,
'ref_fourn' => $refFourn
];
}
// Sort by price
usort($suppliers, function($a, $b) {
return $a['price'] <=> $b['price'];
});
}
// Get unit label
$stockUnit = 'Stk';
if (!empty($obj->fk_unit)) {
$unitSql = "SELECT label FROM ".MAIN_DB_PREFIX."c_units WHERE rowid = ".((int) $obj->fk_unit);
$unitRes = $db->query($unitSql);
if ($unitRes && $unitObj = $db->fetch_object($unitRes)) {
$stockUnit = $unitObj->label;
}
}
$products[] = [
'id' => $obj->rowid,
'ref' => $obj->ref,
'label' => $obj->label,
'barcode' => $obj->barcode,
'stock' => (float) $obj->stock_reel,
'stock_unit' => $stockUnit,
'suppliers' => $suppliers
];
}
}
echo json_encode([
'success' => true,
'products' => $products
]);

147
ajax/updateorderline.php Normal file
View file

@ -0,0 +1,147 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* AJAX: Update or delete order line
*/
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1');
}
if (!defined('NOREQUIREMENU')) {
define('NOREQUIREMENU', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
if (!defined('NOREQUIREAJAX')) {
define('NOREQUIREAJAX', '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(json_encode(['success' => false, 'error' => 'Failed to load Dolibarr']));
}
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
header('Content-Type: application/json; charset=utf-8');
// Security check
if (!$user->hasRight('fournisseur', 'commande', 'creer') && !$user->hasRight('supplier_order', 'creer')) {
echo json_encode(['success' => false, 'error' => 'Access denied']);
exit;
}
$action = GETPOST('action', 'alpha');
$orderId = GETPOSTINT('order_id');
$lineId = GETPOSTINT('line_id');
if (empty($orderId) || empty($lineId)) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
$order = new CommandeFournisseur($db);
if ($order->fetch($orderId) <= 0) {
echo json_encode(['success' => false, 'error' => 'Order not found']);
exit;
}
// Only allow changes to draft orders
if ($order->statut != 0) {
echo json_encode(['success' => false, 'error' => 'Order is not a draft']);
exit;
}
// Find line
$targetLine = null;
foreach ($order->lines as $line) {
if ($line->id == $lineId) {
$targetLine = $line;
break;
}
}
if (!$targetLine) {
echo json_encode(['success' => false, 'error' => 'Line not found']);
exit;
}
if ($action === 'delete') {
$result = $order->deleteLine($lineId);
if ($result < 0) {
echo json_encode(['success' => false, 'error' => 'Failed to delete line: ' . $order->error]);
exit;
}
// Check if order is now empty - if so, delete it
$order->fetch($orderId);
if (count($order->lines) == 0) {
$order->delete($user);
echo json_encode([
'success' => true,
'message' => 'Line and empty order deleted',
'order_deleted' => true
]);
exit;
}
echo json_encode([
'success' => true,
'message' => 'Line deleted',
'order_deleted' => false
]);
} elseif ($action === 'update') {
$newQty = GETPOSTFLOAT('qty');
if ($newQty <= 0) {
echo json_encode(['success' => false, 'error' => 'Quantity must be greater than 0']);
exit;
}
$result = $order->updateline(
$lineId,
$targetLine->desc,
$targetLine->subprice,
$newQty,
$targetLine->remise_percent,
$targetLine->tva_tx,
$targetLine->localtax1_tx ?: 0,
$targetLine->localtax2_tx ?: 0,
'HT',
0,
0,
$targetLine->product_type ?: 0,
false,
null,
null,
0,
$targetLine->ref_fourn
);
if ($result < 0) {
echo json_encode(['success' => false, 'error' => 'Failed to update line: ' . $order->error]);
exit;
}
echo json_encode([
'success' => true,
'message' => 'Line updated',
'new_qty' => $newQty
]);
} else {
echo json_encode(['success' => false, 'error' => 'Unknown action']);
}

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'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '4.7';
$this->version = '5.3';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';

View file

@ -600,3 +600,592 @@
min-height: 48px;
}
}
/* ============================================
ORDER VIEW ENHANCEMENTS
============================================ */
/* Tools row (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: 10px 12px;
background: var(--colorbacklinepair1, #f8f8f8);
border: 1px solid var(--colorborder, #ddd);
border-radius: 6px;
font-size: 13px;
color: var(--colortextlink, #333);
cursor: pointer;
transition: all 0.2s ease;
}
.tool-btn:hover {
background: var(--colorbacklinepair2, #f0f0f0);
}
.tool-btn:active {
transform: scale(0.98);
}
.tool-btn svg {
width: 16px;
height: 16px;
}
/* Search Modal */
.search-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: flex-start;
justify-content: center;
z-index: 10000;
padding: 20px;
padding-top: 60px;
}
.search-modal-content {
background: #fff;
border-radius: 12px;
width: 100%;
max-width: 450px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.search-modal-header {
padding: 15px;
border-bottom: 1px solid var(--colorborder, #ddd);
display: flex;
align-items: center;
gap: 10px;
}
.search-modal-header input {
flex: 1;
padding: 10px 12px;
border: 1px solid var(--colorborder, #ddd);
border-radius: 6px;
font-size: 16px;
}
.search-modal-header input:focus {
border-color: var(--butactionbg, #0077b3);
outline: none;
}
.search-close-btn {
padding: 8px;
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
}
.search-results {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.search-result-item {
padding: 12px;
border: 1px solid var(--colorborder, #ddd);
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.search-result-item:hover {
background: var(--colorbacklinepair2, #f5f5f5);
}
.search-result-label {
font-weight: 600;
font-size: 14px;
margin-bottom: 4px;
}
.search-result-info {
font-size: 12px;
color: #666;
display: flex;
gap: 15px;
}
.search-loading {
text-align: center;
padding: 20px;
color: #666;
}
/* Freetext Modal */
.freetext-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 20px;
}
.freetext-modal-content {
background: #fff;
border-radius: 12px;
padding: 20px;
width: 100%;
max-width: 400px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.freetext-modal-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.freetext-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.freetext-form label {
font-size: 13px;
font-weight: 500;
color: #666;
margin-bottom: 4px;
display: block;
}
.freetext-form input,
.freetext-form select {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--colorborder, #ddd);
border-radius: 6px;
font-size: 15px;
box-sizing: border-box;
}
.freetext-form input:focus,
.freetext-form select:focus {
border-color: var(--butactionbg, #0077b3);
outline: none;
}
.freetext-row {
display: flex;
gap: 10px;
}
.freetext-row > div {
flex: 1;
}
.freetext-buttons {
display: flex;
gap: 10px;
margin-top: 10px;
}
.freetext-buttons .action-btn {
flex: 1;
margin-top: 0;
}
/* ============================================
SWIPEABLE ORDER OVERVIEW
============================================ */
.order-view-container {
position: relative;
overflow: hidden;
width: 100%;
}
/* Swipe indicator */
.swipe-indicator {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
padding: 8px;
background: rgba(0,0,0,0.1);
border-radius: 50%;
font-size: 16px;
color: #666;
pointer-events: none;
opacity: 0.6;
animation: swipeHint 2s ease-in-out infinite;
z-index: 5;
}
@keyframes swipeHint {
0%, 100% { transform: translateY(-50%) translateX(0); }
50% { transform: translateY(-50%) translateX(-5px); }
}
.order-view-container.swiped .swipe-indicator {
display: none;
}
/* Orders panel (slides in from right) */
.orders-panel {
position: absolute;
top: 0;
left: 100%;
width: 100%;
height: 100%;
background: var(--colorbackbody, #fff);
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: 12px 15px;
background: var(--colorbacktitle1, #f0f0f0);
border-bottom: 1px solid var(--colorborder, #ddd);
display: flex;
align-items: center;
justify-content: space-between;
}
.orders-panel-title {
font-weight: 600;
font-size: 15px;
}
.orders-panel-back {
padding: 6px 12px;
background: none;
border: 1px solid var(--colorborder, #ddd);
border-radius: 4px;
font-size: 13px;
cursor: pointer;
}
/* Horizontal order scroller */
.orders-scroller {
flex: 1;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding: 15px;
-webkit-overflow-scrolling: touch;
}
.orders-scroller::-webkit-scrollbar {
height: 6px;
}
.orders-scroller::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
/* Order card */
.order-card {
display: inline-block;
vertical-align: top;
width: 180px;
min-height: 100px;
padding: 12px;
margin-right: 10px;
background: var(--colorbacklinepair1, #f8f8f8);
border: 2px solid var(--colorborder, #ddd);
border-radius: 8px;
cursor: pointer;
white-space: normal;
transition: all 0.2s ease;
}
.order-card:hover {
border-color: var(--butactionbg, #0077b3);
}
.order-card.active {
border-color: var(--butactionbg, #0077b3);
background: rgba(0, 119, 179, 0.05);
}
.order-card.direkt {
font-weight: bold;
}
.order-card.direkt .order-card-ref {
font-weight: 700;
}
.order-card-supplier {
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
overflow: hidden;
text-overflow: ellipsis;
}
.order-card-ref {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.order-card-status {
display: inline-block;
font-size: 10px;
padding: 2px 6px;
border-radius: 3px;
background: #e9ecef;
color: #495057;
}
.order-card-status.draft {
background: #fff3cd;
color: #856404;
}
.order-card-status.validated {
background: #d4edda;
color: #155724;
}
.order-card-info {
margin-top: 8px;
font-size: 11px;
color: #888;
}
/* Order detail panel */
.order-detail-panel {
position: absolute;
top: 0;
left: 200%;
width: 100%;
height: 100%;
background: var(--colorbackbody, #fff);
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: 12px 15px;
background: var(--colorbacktitle1, #f0f0f0);
border-bottom: 1px solid var(--colorborder, #ddd);
display: flex;
align-items: center;
justify-content: space-between;
}
.order-detail-title {
font-weight: 600;
font-size: 15px;
}
.order-detail-back {
padding: 6px 12px;
background: none;
border: 1px solid var(--colorborder, #ddd);
border-radius: 4px;
font-size: 13px;
cursor: pointer;
}
.order-detail-content {
flex: 1;
overflow-y: auto;
padding: 15px;
}
/* Order lines */
.order-line {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: var(--colorbacklinepair1, #f8f8f8);
border: 1px solid var(--colorborder, #ddd);
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.order-line:hover {
background: var(--colorbacklinepair2, #f0f0f0);
}
.order-line-info {
flex: 1;
min-width: 0;
}
.order-line-label {
font-size: 14px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-line-ref {
font-size: 11px;
color: #888;
margin-top: 2px;
}
.order-line-qty {
font-size: 16px;
font-weight: 700;
color: var(--butactionbg, #0077b3);
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.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 20px;
}
.line-edit-content {
background: #fff;
border-radius: 12px;
padding: 20px;
width: 100%;
max-width: 350px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.line-edit-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
}
.line-edit-info {
background: var(--colorbacklinepair1, #f8f8f8);
padding: 12px;
border-radius: 6px;
margin-bottom: 15px;
}
.line-edit-stock {
font-size: 13px;
color: #666;
margin-bottom: 8px;
}
.line-edit-qty {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.line-edit-qty label {
font-size: 14px;
font-weight: 500;
}
.line-edit-qty input {
width: 80px;
padding: 8px;
text-align: center;
font-size: 16px;
font-weight: 600;
border: 1px solid var(--colorborder, #ddd);
border-radius: 6px;
}
.line-edit-buttons {
display: flex;
gap: 10px;
}
.line-edit-buttons .action-btn {
flex: 1;
margin-top: 0;
padding: 12px;
}
/* No orders state */
.no-orders {
text-align: center;
padding: 30px 20px;
color: #666;
}
/* Loading spinner */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #ddd;
border-top-color: var(--butactionbg, #0077b3);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Mobile adjustments for order view */
@media (max-width: 768px) {
.order-card {
width: 160px;
}
.search-modal {
padding: 10px;
padding-top: 20px;
}
.search-modal-content {
max-height: 90vh;
}
}

File diff suppressed because it is too large Load diff

View file

@ -81,3 +81,19 @@ EnableSound = Ton bei Scan
EnableSoundDesc = Akustisches Signal bei erfolgreichem Scan
MobileAccess = Mobiler Zugriff
ScanQRCodeOrOpenURL = QR-Code scannen oder URL oeffnen
# Order View Extensions
ProductSearch = Produktsuche
FreetextPosition = Freitext-Position
Orders = Bestellungen
Back = Zurueck
NoOrders = Keine Bestellungen
Positions = Positionen
Pieces = Stueck
DeletePosition = Position loeschen
DeleteConfirm = Position wirklich loeschen?
PositionDeleted = Position geloescht
QuantityUpdated = Anzahl aktualisiert
Description = Beschreibung
NetPrice = Preis (netto)
AddLine = Hinzufuegen

478
pwa.php
View file

@ -345,18 +345,20 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
}
.scanner-controls {
padding: 10px;
padding: 8px 10px;
background: #444;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.scan-btn {
min-width: 180px;
padding: 14px 28px;
font-size: 16px;
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
border: none;
border-radius: 8px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
@ -371,6 +373,46 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
color: white;
}
/* 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; }
.scan-tool-btn.freetext-btn { order: 3; }
.scan-buttons-group { order: 2; display: flex; gap: 8px; }
/* 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);
@ -477,6 +519,145 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
.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: 10px; }
.print-modal-buttons .action-btn { flex: 1; margin: 0; padding: 14px; font-size: 15px; }
/* 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>
@ -548,11 +729,30 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
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 -->
@ -562,9 +762,20 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
<video id="scanner-video" playsinline></video>
<div class="scan-region-highlight"></div>
</div>
<div class="scanner-controls">
<button type="button" id="start-scan-btn" class="scan-btn start">Scannen starten</button>
<button type="button" id="stop-scan-btn" class="scan-btn stop hidden">Scannen stoppen</button>
<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>
<!-- 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>
</div>
</div>
@ -573,6 +784,11 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
<span id="last-scan-code">-</span>
</div>
<!-- Swipe hint for order mode -->
<div id="swipe-hint" class="swipe-hint" style="display:none;">
<span>&larr; Bestellungen</span>
</div>
<div id="result-area" class="scanner-result hidden"></div>
</main>
</div>
@ -813,6 +1029,249 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
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">&times;</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, "&#39;")}'>
<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-now">Drucken</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);
// 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) {
@ -824,6 +1283,7 @@ $colormain = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#0077b3');
initApp();
</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="<?php echo dol_buildpath('/handybarcodescanner/js/scanner.js', 1); ?>"></script>
</body>
</html>

5
sw.js
View file

@ -1,12 +1,13 @@
// Service Worker for HandyBarcodeScanner PWA
const CACHE_NAME = 'scanner-v4.4';
const CACHE_NAME = 'scanner-v5.3';
const ASSETS = [
'pwa.php',
'css/scanner.css',
'js/scanner.js',
'img/icon-192.png',
'img/icon-512.png',
'https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js'
'https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js',
'https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js'
];
// Install - cache assets