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:
parent
74728a71d1
commit
5f9c522db2
14 changed files with 589 additions and 36 deletions
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
17
ChangeLog.md
17
ChangeLog.md
|
|
@ -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
0
ajax/addfreetextline.php
Normal file → Executable file
74
ajax/deleteorder.php
Normal file
74
ajax/deleteorder.php
Normal 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
4
ajax/getorderlines.php
Normal file → Executable 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
0
ajax/getorders.php
Normal file → Executable file
0
ajax/searchproduct.php
Normal file → Executable file
0
ajax/searchproduct.php
Normal file → Executable file
6
ajax/updateorderline.php
Normal file → Executable file
6
ajax/updateorderline.php
Normal file → Executable 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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
141
js/scanner.js
141
js/scanner.js
|
|
@ -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', {
|
||||
|
|
|
|||
4
pwa.php
4
pwa.php
|
|
@ -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>← Bestellungen</span>
|
||||
</div>
|
||||
|
||||
<div id="result-area" class="scanner-result hidden"></div>
|
||||
</main>
|
||||
|
|
|
|||
152
pwa_login.php
Executable file
152
pwa_login.php
Executable 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
136
pwa_verify.php
Executable 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
2
sw.js
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue