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:
parent
48bf9411dc
commit
74728a71d1
14 changed files with 2654 additions and 36 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
bin/
|
||||
28
ChangeLog.md
28
ChangeLog.md
|
|
@ -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)
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -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
144
ajax/addfreetextline.php
Normal 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
104
ajax/getorderlines.php
Normal 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
111
ajax/getorders.php
Normal 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
121
ajax/searchproduct.php
Normal 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
147
ajax/updateorderline.php
Normal 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']);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
589
css/scanner.css
589
css/scanner.css
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
924
js/scanner.js
924
js/scanner.js
File diff suppressed because it is too large
Load diff
|
|
@ -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
478
pwa.php
|
|
@ -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>← 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">×</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-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
5
sw.js
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue