v5.1: Bestellungen löschen, Freitext-Bearbeitung, Dark Theme Fix

Bestellungen verwalten:
- Lösch-Button an Entwurfs-Bestellungen mit Bestätigungsdialog
- Freitext-Zeilen: Beschreibung und Menge änderbar
- Letzter Freitext-Lieferant wird für nächsten Eintrag gemerkt

Dark Theme:
- Bestellzeilen korrekt lesbar (war weiß auf hell)
- Dialoge mit konsistenten Dark Theme Farben
- Aktive Bestellung besser hervorgehoben

Entfernt:
- Swipe-Hinweis-Button (überflüssig)

Neuer AJAX-Endpoint:
- deleteorder.php

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-25 13:53:24 +01:00
parent 74728a71d1
commit 5f9c522db2
14 changed files with 589 additions and 36 deletions

0
.gitignore vendored Normal file → Executable file
View file

View file

@ -25,9 +25,24 @@
### 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
## 5.1
### Bestellungen verwalten
- **Bestellung löschen**: Lösch-Button (Papierkorb-Icon) an jeder Entwurfs-Bestellung
- **Bestätigungsdialog**: Sicherheitsabfrage vor dem Löschen einer Bestellung
- **Freitext-Zeilen bearbeiten**: Beschreibung und Menge von Freitext-Positionen änderbar
- **Lieferant merken**: Zuletzt verwendeter Freitext-Lieferant wird für nächsten Eintrag vorausgewählt
### Dark Theme Verbesserungen
- **Bestellzeilen**: Korrektes Styling im Dark Theme (war weiß/unleserlich)
- **Dialoge**: Alle Dialoge mit konsistenten Dark Theme Farben
- **Aktive Bestellung**: Bessere Hervorhebung der aktiven Bestellung
### Entfernt
- Swipe-Hinweis-Button (überflüssig, Swipe ist intuitiv)
## 4.7
- PWA-Link auf der Scanner-Seite angezeigt (korrekter externer Hostname statt interner IP)

0
ajax/addfreetextline.php Normal file → Executable file
View file

74
ajax/deleteorder.php Normal file
View file

@ -0,0 +1,74 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* AJAX: Delete entire 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';
header('Content-Type: application/json; charset=utf-8');
// Security check
if (!$user->hasRight('fournisseur', 'commande', 'supprimer') && !$user->hasRight('supplier_order', 'supprimer')) {
echo json_encode(['success' => false, 'error' => 'Keine Berechtigung zum Löschen']);
exit;
}
$orderId = GETPOSTINT('order_id');
if (empty($orderId)) {
echo json_encode(['success' => false, 'error' => 'Missing order_id']);
exit;
}
$order = new CommandeFournisseur($db);
if ($order->fetch($orderId) <= 0) {
echo json_encode(['success' => false, 'error' => 'Bestellung nicht gefunden']);
exit;
}
// Only allow deletion of draft orders
if ($order->statut != 0) {
echo json_encode(['success' => false, 'error' => 'Nur Entwürfe können gelöscht werden']);
exit;
}
$result = $order->delete($user);
if ($result < 0) {
echo json_encode(['success' => false, 'error' => 'Löschen fehlgeschlagen: ' . $order->error]);
exit;
}
echo json_encode([
'success' => true,
'message' => 'Bestellung gelöscht'
]);

4
ajax/getorderlines.php Normal file → Executable file
View file

@ -82,11 +82,13 @@ foreach ($order->lines as $line) {
'product_id' => (int) $line->fk_product,
'product_ref' => $productRef,
'product_label' => $productLabel,
'description' => $line->desc ?: '',
'qty' => (float) $line->qty,
'price' => (float) $line->subprice,
'total_ht' => (float) $line->total_ht,
'stock' => $stock,
'ref_fourn' => $line->ref_fourn ?: ''
'ref_fourn' => $line->ref_fourn ?: '',
'is_freetext' => empty($line->fk_product) ? true : false
];
}

0
ajax/getorders.php Normal file → Executable file
View file

0
ajax/searchproduct.php Normal file → Executable file
View file

6
ajax/updateorderline.php Normal file → Executable file
View file

@ -105,15 +105,19 @@ if ($action === 'delete') {
} elseif ($action === 'update') {
$newQty = GETPOSTFLOAT('qty');
$newDesc = GETPOST('description', 'restricthtml');
if ($newQty <= 0) {
echo json_encode(['success' => false, 'error' => 'Quantity must be greater than 0']);
exit;
}
// Use new description if provided, otherwise keep existing
$description = !empty($newDesc) ? $newDesc : $targetLine->desc;
$result = $order->updateline(
$lineId,
$targetLine->desc,
$description,
$targetLine->subprice,
$newQty,
$targetLine->remise_percent,

View file

@ -933,8 +933,8 @@
}
.order-card.active {
border-color: var(--butactionbg, #0077b3);
background: rgba(0, 119, 179, 0.05);
border-color: var(--butactionbg, #ad8c4f);
background: rgba(173, 140, 79, 0.15);
}
.order-card.direkt {
@ -955,7 +955,7 @@
.order-card-ref {
font-size: 12px;
color: #666;
color: var(--colortextmuted, #b4b4b4);
margin-bottom: 4px;
}
@ -981,7 +981,41 @@
.order-card-info {
margin-top: 8px;
font-size: 11px;
color: #888;
color: var(--colortextmuted, #b4b4b4);
}
/* Order delete button */
.order-delete-btn {
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
border: none;
background: transparent;
color: var(--colortextmuted, #888);
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
opacity: 0.6;
}
.order-delete-btn:hover {
background: var(--danger, #993013);
color: #fff;
opacity: 1;
}
.order-delete-btn svg {
width: 16px;
height: 16px;
}
.order-card {
position: relative;
}
/* Order detail panel */
@ -1037,16 +1071,18 @@
align-items: center;
justify-content: space-between;
padding: 12px;
background: var(--colorbacklinepair1, #f8f8f8);
border: 1px solid var(--colorborder, #ddd);
background: var(--colorbackline, #38393d);
border: 1px solid var(--colorborder, #2b2c2e);
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--colortext, #dcdcdc);
}
.order-line:hover {
background: var(--colorbacklinepair2, #f0f0f0);
background: var(--colorbacktitle, #3b3c3e);
border-color: var(--butactionbg, #ad8c4f);
}
.order-line-info {
@ -1064,7 +1100,7 @@
.order-line-ref {
font-size: 11px;
color: #888;
color: var(--colortextmuted, #b4b4b4);
margin-top: 2px;
}
@ -1092,22 +1128,24 @@
}
.line-edit-content {
background: #fff;
background: var(--colorbackcard, #1d1e20);
color: var(--colortext, #dcdcdc);
border-radius: 12px;
padding: 20px;
width: 100%;
max-width: 350px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
}
.line-edit-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
color: var(--colortext, #dcdcdc);
}
.line-edit-info {
background: var(--colorbacklinepair1, #f8f8f8);
background: var(--colorbackline, #38393d);
padding: 12px;
border-radius: 6px;
margin-bottom: 15px;
@ -1115,10 +1153,32 @@
.line-edit-stock {
font-size: 13px;
color: #666;
color: var(--colortextmuted, #b4b4b4);
margin-bottom: 8px;
}
.line-edit-desc {
margin-bottom: 15px;
}
.line-edit-desc label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: var(--colortext, #dcdcdc);
}
.line-edit-desc input {
width: 100%;
padding: 10px;
font-size: 14px;
border: 1px solid var(--colorborder, #2b2c2e);
border-radius: 6px;
background: var(--colorbackinput, #464646);
color: var(--colortext, #dcdcdc);
}
.line-edit-qty {
display: flex;
align-items: center;
@ -1129,6 +1189,7 @@
.line-edit-qty label {
font-size: 14px;
font-weight: 500;
color: var(--colortext, #dcdcdc);
}
.line-edit-qty input {
@ -1137,8 +1198,10 @@
text-align: center;
font-size: 16px;
font-weight: 600;
border: 1px solid var(--colorborder, #ddd);
border: 1px solid var(--colorborder, #2b2c2e);
border-radius: 6px;
background: var(--colorbackinput, #464646);
color: var(--colortext, #dcdcdc);
}
.line-edit-buttons {

View file

@ -46,6 +46,7 @@
let allSuppliers = [];
let quaggaInitialized = false;
let openDialogCount = 0; // Zaehlt offene Dialoge
let lastFreetextSupplierId = null; // Zuletzt verwendeter Freitext-Lieferant
// Order overview state
let lastOrderId = null;
@ -865,7 +866,7 @@
<div>
<label>Lieferant</label>
<select id="freetext-supplier">
${allSuppliers.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('')}
${allSuppliers.map(s => `<option value="${s.id}" ${s.id == lastFreetextSupplierId ? 'selected' : ''}>${escapeHtml(s.name)}</option>`).join('')}
</select>
</div>
<div>
@ -922,6 +923,9 @@
return;
}
// Lieferant merken für nächstes Mal
lastFreetextSupplierId = supplierId;
showLoading();
fetch(CONFIG.ajaxUrl + 'addfreetextline.php', {
@ -1037,9 +1041,13 @@
const statusClass = order.status === 0 ? 'draft' : 'validated';
const isActive = order.id === lastOrderId;
const direktClass = order.is_direkt ? 'direkt' : '';
const canDelete = order.status === 0; // Nur Entwürfe können gelöscht werden
return `
<div class="order-card ${direktClass} ${isActive ? 'active' : ''}" data-order-id="${order.id}">
${canDelete ? `<button type="button" class="order-delete-btn" data-order-id="${order.id}" title="Bestellung löschen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>` : ''}
<div class="order-card-supplier">${escapeHtml(order.supplier_name)}</div>
<div class="order-card-ref">${escapeHtml(order.ref)}</div>
<div class="order-card-status ${statusClass}">${escapeHtml(order.status_label)}</div>
@ -1048,13 +1056,88 @@
`;
}).join('');
// Bind click events
// Bind click events for order cards
scroller.querySelectorAll('.order-card').forEach(card => {
card.addEventListener('click', function() {
card.addEventListener('click', function(e) {
// Ignoriere Klicks auf den Lösch-Button
if (e.target.closest('.order-delete-btn')) return;
const orderId = parseInt(this.dataset.orderId);
openOrderDetailInModal(orderId);
});
});
// Bind delete button events
scroller.querySelectorAll('.order-delete-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const orderId = parseInt(this.dataset.orderId);
const order = cachedOrders.find(o => o.id === orderId);
showDeleteOrderDialog(orderId, order ? order.ref : '');
});
});
}
function showDeleteOrderDialog(orderId, orderRef) {
const dialog = document.createElement('div');
dialog.className = 'line-edit-dialog';
dialog.id = 'delete-order-dialog';
dialog.innerHTML = `
<div class="line-edit-content">
<div class="line-edit-title">Bestellung löschen?</div>
<div class="line-edit-info" style="text-align:center; margin: 15px 0;">
<strong>${escapeHtml(orderRef)}</strong><br>
<span style="color: var(--danger, #993013);">Diese Aktion kann nicht rückgängig gemacht werden.</span>
</div>
<div class="line-edit-buttons">
<button type="button" class="action-btn btn-secondary" id="delete-order-cancel">Abbrechen</button>
<button type="button" class="action-btn btn-danger" id="delete-order-confirm">Löschen</button>
</div>
</div>
`;
document.body.appendChild(dialog);
function closeDialog() {
dialog.remove();
}
document.getElementById('delete-order-cancel').addEventListener('click', closeDialog);
document.getElementById('delete-order-confirm').addEventListener('click', function() {
closeDialog();
deleteOrder(orderId);
});
dialog.addEventListener('click', function(e) {
if (e.target === dialog) closeDialog();
});
}
function deleteOrder(orderId) {
showLoading();
fetch(CONFIG.ajaxUrl + 'deleteorder.php', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `token=${CONFIG.token}&order_id=${orderId}`
})
.then(res => res.json())
.then(data => {
hideLoading();
if (data.success) {
showToast('Bestellung gelöscht', 'success');
loadOrdersIntoModal();
} else {
showToast(data.error || CONFIG.lang.error, 'error');
}
})
.catch(err => {
hideLoading();
console.error('Delete order error:', err);
showToast(CONFIG.lang.error, 'error');
});
}
function openOrderDetailInModal(orderId) {
@ -1124,15 +1207,24 @@
function showLineEditDialogInModal(line, orderId) {
pauseScanner();
const isFreetext = line.is_freetext || !line.product_id;
const dialog = document.createElement('div');
dialog.className = 'line-edit-dialog';
dialog.id = 'line-edit-dialog';
dialog.innerHTML = `
<div class="line-edit-content">
<div class="line-edit-title">${escapeHtml(line.product_label)}</div>
<div class="line-edit-info">
<div class="line-edit-stock">Lagerbestand: <strong>${line.stock}</strong></div>
</div>
<div class="line-edit-title">${isFreetext ? 'Freitext-Position' : escapeHtml(line.product_label)}</div>
${isFreetext ? `
<div class="line-edit-desc">
<label>Beschreibung:</label>
<input type="text" id="line-desc-input" value="${escapeHtml(line.description || line.product_label)}">
</div>
` : `
<div class="line-edit-info">
<div class="line-edit-stock">Lagerbestand: <strong>${line.stock}</strong></div>
</div>
`}
<div class="line-edit-qty">
<label>Anzahl:</label>
<input type="number" id="line-qty-input" value="${line.qty}" min="1">
@ -1155,7 +1247,9 @@
document.getElementById('line-save').addEventListener('click', () => {
const newQty = parseFloat(document.getElementById('line-qty-input').value) || 1;
updateOrderLineInModal(orderId, line.id, 'update', newQty);
const descInput = document.getElementById('line-desc-input');
const newDesc = descInput ? descInput.value.trim() : null;
updateOrderLineInModal(orderId, line.id, 'update', newQty, newDesc);
closeDialog();
});
@ -1173,12 +1267,15 @@
});
}
function updateOrderLineInModal(orderId, lineId, action, qty = 0) {
function updateOrderLineInModal(orderId, lineId, action, qty = 0, description = null) {
showLoading();
let body = `token=${CONFIG.token}&order_id=${orderId}&line_id=${lineId}&action=${action}`;
if (action === 'update') {
body += `&qty=${qty}`;
if (description !== null) {
body += `&description=${encodeURIComponent(description)}`;
}
}
fetch(CONFIG.ajaxUrl + 'updateorderline.php', {
@ -1410,15 +1507,24 @@
function showLineEditDialog(line, orderId) {
pauseScanner();
const isFreetext = line.is_freetext || !line.product_id;
const dialog = document.createElement('div');
dialog.className = 'line-edit-dialog';
dialog.id = 'line-edit-dialog';
dialog.innerHTML = `
<div class="line-edit-content">
<div class="line-edit-title">${escapeHtml(line.product_label)}</div>
<div class="line-edit-info">
<div class="line-edit-stock">Lagerbestand: <strong>${line.stock}</strong></div>
</div>
<div class="line-edit-title">${isFreetext ? 'Freitext-Position' : escapeHtml(line.product_label)}</div>
${isFreetext ? `
<div class="line-edit-desc">
<label>Beschreibung:</label>
<input type="text" id="line-desc-input" value="${escapeHtml(line.description || line.product_label)}">
</div>
` : `
<div class="line-edit-info">
<div class="line-edit-stock">Lagerbestand: <strong>${line.stock}</strong></div>
</div>
`}
<div class="line-edit-qty">
<label>Anzahl:</label>
<input type="number" id="line-qty-input" value="${line.qty}" min="1">
@ -1441,7 +1547,9 @@
document.getElementById('line-save').addEventListener('click', () => {
const newQty = parseFloat(document.getElementById('line-qty-input').value) || 1;
updateOrderLine(orderId, line.id, 'update', newQty);
const descInput = document.getElementById('line-desc-input');
const newDesc = descInput ? descInput.value.trim() : null;
updateOrderLine(orderId, line.id, 'update', newQty, newDesc);
closeDialog();
});
@ -1459,12 +1567,15 @@
});
}
function updateOrderLine(orderId, lineId, action, qty = 0) {
function updateOrderLine(orderId, lineId, action, qty = 0, description = null) {
showLoading();
let body = `token=${CONFIG.token}&order_id=${orderId}&line_id=${lineId}&action=${action}`;
if (action === 'update') {
body += `&qty=${qty}`;
if (description !== null) {
body += `&description=${encodeURIComponent(description)}`;
}
}
fetch(CONFIG.ajaxUrl + 'updateorderline.php', {

View file

@ -784,10 +784,6 @@ $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>

152
pwa_login.php Executable file
View file

@ -0,0 +1,152 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* AJAX: PWA Login - Authentifiziert Benutzer und gibt Token zurueck
*/
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1');
}
if (!defined('NOREQUIREMENU')) {
define('NOREQUIREMENU', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
if (!defined('NOREQUIREAJAX')) {
define('NOREQUIREAJAX', '1');
}
// Wichtig: Kein Login erforderlich fuer diese Seite
if (!defined('NOLOGIN')) {
define('NOLOGIN', '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.'/user/class/user.class.php';
header('Content-Type: application/json; charset=utf-8');
// Nur POST erlaubt
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$login = GETPOST('login', 'alphanohtml');
$password = GETPOST('password', 'none'); // 'none' = kein Filter fuer Passwort
if (empty($login) || empty($password)) {
echo json_encode(['success' => false, 'error' => 'Login und Passwort erforderlich']);
exit;
}
// Benutzer authentifizieren
$userobj = new User($db);
$result = $userobj->fetch('', $login);
if ($result <= 0) {
// Benutzer nicht gefunden - generische Fehlermeldung aus Sicherheitsgruenden
sleep(1); // Brute-Force-Schutz
echo json_encode(['success' => false, 'error' => 'Login fehlgeschlagen']);
exit;
}
// Passwort pruefen
$passOk = false;
// Methode 1: password_verify (moderne Dolibarr-Versionen)
if (function_exists('password_verify') && !empty($userobj->pass_indatabase_crypted)) {
$passOk = password_verify($password, $userobj->pass_indatabase_crypted);
}
// Methode 2: dol_hash (aeltere Versionen)
if (!$passOk && !empty($userobj->pass_indatabase_crypted)) {
$passOk = (dol_hash($password) === $userobj->pass_indatabase_crypted);
}
// Methode 3: MD5 (sehr alte Installationen)
if (!$passOk && !empty($userobj->pass_indatabase_crypted)) {
$passOk = (md5($password) === $userobj->pass_indatabase_crypted);
}
if (!$passOk) {
sleep(1); // Brute-Force-Schutz
echo json_encode(['success' => false, 'error' => 'Login fehlgeschlagen']);
exit;
}
// Benutzer ist aktiv?
if ($userobj->statut != 1) {
echo json_encode(['success' => false, 'error' => 'Benutzer deaktiviert']);
exit;
}
// Rechte pruefen
$userobj->getrights();
$hasAccess = false;
if ($userobj->hasRight('handybarcodescanner', 'use')) {
$hasAccess = true;
} elseif ($userobj->hasRight('fournisseur', 'commande', 'creer') || $userobj->hasRight('supplier_order', 'creer')) {
$hasAccess = true;
}
if (!$hasAccess) {
echo json_encode(['success' => false, 'error' => 'Keine Berechtigung fuer Scanner']);
exit;
}
// Session starten und Benutzer einloggen
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Dolibarr Session-Variablen setzen
$_SESSION['dol_login'] = $userobj->login;
$_SESSION['dol_authmode'] = 'dolibarr';
$_SESSION['dol_tz'] = GETPOST('tz', 'alpha');
$_SESSION['dol_entity'] = $conf->entity;
// Token generieren fuer 15 Tage
$tokenData = [
'user_id' => $userobj->id,
'login' => $userobj->login,
'entity' => $conf->entity,
'created' => time(),
'expires' => time() + (15 * 24 * 60 * 60), // 15 Tage
'hash' => bin2hex(random_bytes(16))
];
// Token als Base64-encoded JSON (nicht sicher fuer echte Auth, aber reicht fuer PWA-Cache)
$pwaToken = base64_encode(json_encode($tokenData));
// Dolibarr CSRF-Token
$csrfToken = newToken();
echo json_encode([
'success' => true,
'pwa_token' => $pwaToken,
'csrf_token' => $csrfToken,
'user' => [
'id' => $userobj->id,
'login' => $userobj->login,
'firstname' => $userobj->firstname,
'lastname' => $userobj->lastname
],
'expires' => $tokenData['expires'],
'expires_human' => date('Y-m-d H:i:s', $tokenData['expires'])
]);

136
pwa_verify.php Executable file
View file

@ -0,0 +1,136 @@
<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* AJAX: PWA Token Verify - Prueft gespeicherten Token und startet Session
*/
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1');
}
if (!defined('NOREQUIREMENU')) {
define('NOREQUIREMENU', '1');
}
if (!defined('NOREQUIREHTML')) {
define('NOREQUIREHTML', '1');
}
if (!defined('NOREQUIREAJAX')) {
define('NOREQUIREAJAX', '1');
}
if (!defined('NOLOGIN')) {
define('NOLOGIN', '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.'/user/class/user.class.php';
header('Content-Type: application/json; charset=utf-8');
// Token aus Header oder POST
$pwaToken = '';
if (!empty($_SERVER['HTTP_X_PWA_TOKEN'])) {
$pwaToken = $_SERVER['HTTP_X_PWA_TOKEN'];
} else {
$pwaToken = GETPOST('pwa_token', 'alphanohtml');
}
if (empty($pwaToken)) {
echo json_encode(['success' => false, 'error' => 'Token fehlt', 'need_login' => true]);
exit;
}
// Token dekodieren
$tokenJson = base64_decode($pwaToken);
if ($tokenJson === false) {
echo json_encode(['success' => false, 'error' => 'Ungueltiger Token', 'need_login' => true]);
exit;
}
$tokenData = json_decode($tokenJson, true);
if (!$tokenData || !isset($tokenData['user_id']) || !isset($tokenData['expires'])) {
echo json_encode(['success' => false, 'error' => 'Token-Format ungueltig', 'need_login' => true]);
exit;
}
// Ablauf pruefen
if ($tokenData['expires'] < time()) {
echo json_encode(['success' => false, 'error' => 'Token abgelaufen', 'need_login' => true]);
exit;
}
// Benutzer laden
$userobj = new User($db);
$result = $userobj->fetch($tokenData['user_id']);
if ($result <= 0) {
echo json_encode(['success' => false, 'error' => 'Benutzer nicht gefunden', 'need_login' => true]);
exit;
}
// Benutzer noch aktiv?
if ($userobj->statut != 1) {
echo json_encode(['success' => false, 'error' => 'Benutzer deaktiviert', 'need_login' => true]);
exit;
}
// Login stimmt ueberein?
if ($userobj->login !== $tokenData['login']) {
echo json_encode(['success' => false, 'error' => 'Token ungueltig', 'need_login' => true]);
exit;
}
// Rechte pruefen
$userobj->getrights();
$hasAccess = false;
if ($userobj->hasRight('handybarcodescanner', 'use')) {
$hasAccess = true;
} elseif ($userobj->hasRight('fournisseur', 'commande', 'creer') || $userobj->hasRight('supplier_order', 'creer')) {
$hasAccess = true;
}
if (!$hasAccess) {
echo json_encode(['success' => false, 'error' => 'Keine Berechtigung', 'need_login' => true]);
exit;
}
// Session starten/aktualisieren
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION['dol_login'] = $userobj->login;
$_SESSION['dol_authmode'] = 'dolibarr';
$_SESSION['dol_entity'] = $tokenData['entity'] ?? $conf->entity;
// Neuen CSRF-Token generieren
$csrfToken = newToken();
// Verbleibende Zeit berechnen
$remainingDays = ceil(($tokenData['expires'] - time()) / (24 * 60 * 60));
echo json_encode([
'success' => true,
'csrf_token' => $csrfToken,
'user' => [
'id' => $userobj->id,
'login' => $userobj->login,
'firstname' => $userobj->firstname,
'lastname' => $userobj->lastname
],
'expires' => $tokenData['expires'],
'remaining_days' => $remainingDays
]);

2
sw.js
View file

@ -1,5 +1,5 @@
// Service Worker for HandyBarcodeScanner PWA
const CACHE_NAME = 'scanner-v5.3';
const CACHE_NAME = 'scanner-v5.4';
const ASSETS = [
'pwa.php',
'css/scanner.css',