From 4ca3ea5deb3a140155adfa300957d18987371fc6 Mon Sep 17 00:00:00 2001 From: data Date: Sat, 21 Mar 2026 21:42:56 +0100 Subject: [PATCH] PWA: Dezimal-Mengen, STZ-Freigabe, Notiz-Trennung, qty_done=1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mengenanzeige klickbar für Dezimaleingabe (Komma), +/- bleiben Ganzzahl - Freigeben/Wiedereröffnen-Button für einzelne Stundenzettel - Warnung bei Freigabe ohne Leistung mit Service-Auswahl-Dialog (Standard-Dienstleistung des Kunden vorausgewählt) - API: validate_stz und setdraft_stz Endpunkte - API: default_service_id/label im get_order_context - Produktübernahme: qty_done Standard auf 1 statt 0 - Merkzettel auf Produktliste: nur Anzeige + Abhaken, kein Hinzufügen - Scroll-Position nach Panel-Neurendern zurücksetzen Co-Authored-By: Claude Opus 4.5 --- ajax/pwa_api.php | 82 +++++++++++++++- css/pwa.css | 42 +++++++++ js/pwa.js | 237 +++++++++++++++++++++++++++++++++++++++-------- pwa.php | 4 +- 4 files changed, 321 insertions(+), 44 deletions(-) diff --git a/ajax/pwa_api.php b/ajax/pwa_api.php index 959e2e4..22d615c 100644 --- a/ajax/pwa_api.php +++ b/ajax/pwa_api.php @@ -30,6 +30,7 @@ require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php'; require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; dol_include_once('/stundenzettel/class/stundenzettel.class.php'); +dol_include_once('/stundenzettel/lib/stundenzettel.lib.php'); header('Content-Type: application/json; charset=UTF-8'); @@ -237,6 +238,20 @@ switch ($action) { $customer = new Societe($db); $customer->fetch($order->socid); + // Standard-Dienstleistung des Kunden ermitteln + $defaultServiceId = 0; + $defaultServiceLabel = ''; + if (isset($customer->array_options['options_stundenzettel_default_service'])) { + $defaultServiceId = (int)$customer->array_options['options_stundenzettel_default_service']; + if ($defaultServiceId > 0) { + $sqlDS = "SELECT ref, label FROM ".MAIN_DB_PREFIX."product WHERE rowid = ".((int)$defaultServiceId); + $resDS = $db->query($sqlDS); + if ($resDS && ($objDS = $db->fetch_object($resDS))) { + $defaultServiceLabel = $objDS->ref.' - '.$objDS->label; + } + } + } + // Auftragsdaten $response['order'] = array( 'id' => (int)$order->id, @@ -244,7 +259,9 @@ switch ($action) { 'date' => dol_print_date($order->date_commande, 'day'), 'customer_name' => $customer->name, 'customer_id' => (int)$customer->id, - 'status' => (int)$order->statut + 'status' => (int)$order->statut, + 'default_service_id' => $defaultServiceId, + 'default_service_label' => $defaultServiceLabel ); // Stundenzettel finden (per ID oder letzten Draft) @@ -1497,7 +1514,7 @@ switch ($action) { $objLine->rowid, // fk_commandedet null, $objLine->qty, // qty_original - 0, // qty_done + 1, // qty_done (Standard: 1) 'order', $objLine->description ); @@ -1543,7 +1560,7 @@ switch ($action) { null, // fk_commandedet (kein Auftragsbezug) null, $qty, // qty_original = Zielmenge - 0, // qty_done + 1, // qty_done (Standard: 1) 'added', $description ?: $productLabel ); @@ -1556,6 +1573,65 @@ switch ($action) { $response['added'] = $added; break; + // ---- Stundenzettel freigeben (validieren) ---- + case 'validate_stz': + if (!$canWrite) { + $response['error'] = 'Keine Schreibberechtigung'; + break; + } + + $stzId = GETPOST('stz_id', 'int'); + $stz = new Stundenzettel($db); + if ($stz->fetch($stzId) <= 0) { + $response['error'] = 'Stundenzettel nicht gefunden'; + break; + } + if ($stz->status != Stundenzettel::STATUS_DRAFT) { + $response['error'] = 'Stundenzettel ist nicht im Entwurf'; + break; + } + if (!canEditStz($stz, $user, $canWriteAll)) { + $response['error'] = 'Keine Berechtigung'; + break; + } + + $result = $stz->validate($user); + if ($result > 0) { + // Netto-Wert aller Stundenzettel des Auftrags neu berechnen + updateOrderNettoSTZ($db, $stz->fk_commande); + $response['success'] = true; + } else { + $response['error'] = $stz->error ?: 'Fehler beim Freigeben'; + } + break; + + // ---- Stundenzettel zurueck auf Entwurf setzen ---- + case 'setdraft_stz': + if (!$canWrite) { + $response['error'] = 'Keine Schreibberechtigung'; + break; + } + + $stzId = GETPOST('stz_id', 'int'); + $stz = new Stundenzettel($db); + if ($stz->fetch($stzId) <= 0) { + $response['error'] = 'Stundenzettel nicht gefunden'; + break; + } + if ($stz->status == Stundenzettel::STATUS_DRAFT) { + $response['error'] = 'Stundenzettel ist bereits im Entwurf'; + break; + } + + $result = $stz->setDraft($user); + if ($result > 0) { + updateOrderNettoSTZ($db, $stz->fk_commande); + $response['success'] = true; + } else { + $response['error'] = $stz->error ?: 'Fehler beim Zuruecksetzen'; + } + break; + default: $response['error'] = 'Unbekannte Aktion: '.$action; } diff --git a/css/pwa.css b/css/pwa.css index 0d2b5df..78b2d96 100644 --- a/css/pwa.css +++ b/css/pwa.css @@ -530,6 +530,29 @@ body { font-weight: 600; font-size: 16px; } +.qty-editable { + cursor: pointer; + border-radius: 6px; + padding: 2px 6px; + border: 1px dashed transparent; + transition: border-color 0.15s, background 0.15s; +} +.qty-editable:active { + background: var(--colorbackline); + border-color: var(--colorborder); +} +.qty-inline-input { + width: 56px; + text-align: center; + font-weight: 600; + font-size: 16px; + padding: 2px 4px; + border: 1px solid var(--primary); + border-radius: 6px; + background: var(--colorbackinput); + color: var(--colortext); + outline: none; +} /* === Accordion-Sections === */ .accordion-section { @@ -1269,6 +1292,25 @@ body { width: 100%; } +/* === STZ Aktions-Buttons === */ +.stz-actions { + padding: 8px 0; +} +.warning-box { + background: rgba(255, 193, 7, 0.15); + border: 1px solid rgba(255, 193, 7, 0.4); + border-radius: 8px; + padding: 12px; + font-size: 14px; + line-height: 1.5; +} +.warning-box p { + margin: 0 0 4px 0; +} +.warning-box p:last-child { + margin-bottom: 0; +} + /* Safe-Area fuer Geraete mit Notch */ @supports (padding-bottom: env(safe-area-inset-bottom)) { .fab { diff --git a/js/pwa.js b/js/pwa.js index eba0a47..f5cd423 100644 --- a/js/pwa.js +++ b/js/pwa.js @@ -464,6 +464,8 @@ self.state.customerName = res.order.customer_name; self.state.canWrite = res.can_write; self.state.canEditStz = res.can_edit_stz; + self.state.defaultServiceId = res.order.default_service_id || 0; + self.state.defaultServiceLabel = res.order.default_service_label || ''; if (res.stz) { self.state.stzId = res.stz.id; @@ -652,6 +654,9 @@ this.renderPanelStundenzettel(); this.renderPanelProducts(); this.renderPanelTracking(); + + // Scroll-Position aller Panels nach oben zuruecksetzen + $('.swipe-panel').scrollTop(0); }, // ---- Panel 0: Alle Stundenzettel ---- @@ -892,9 +897,30 @@ html += ''; // accordion-section } + // ---- AKTIONS-BUTTONS (Freigeben / Wiedereroeffnen) ---- + if (self.state.canWrite) { + html += '
'; + if (isDraft) { + html += ''; + } else { + html += ''; + } + html += '
'; + } + $panel.html(html); // ---- Event-Listener ---- + // Freigeben / Wiedereroeffnen + $('#btn-validate-stz').on('click', function() { + self.validateStz(); + }); + $('#btn-setdraft-stz').on('click', function() { + self.showConfirm('Zur\u00fcck auf Entwurf?', 'Stundenzettel wird wieder bearbeitbar.', 'Zur\u00fcck auf Entwurf').then(function(ok) { + if (ok) self.setDraftStz(); + }); + }); + // Leistungen $panel.find('.btn-edit-leistung').on('click', function(e) { e.stopPropagation(); @@ -929,6 +955,52 @@ } }); + // Menge direkt bearbeiten (Klick auf Zahl) + $panel.find('.qty-editable').on('click', function(e) { + e.stopPropagation(); + var $display = $(this); + if ($display.find('input').length) return; // Bereits im Edit-Modus + var id = $display.data('id'); + var qty = parseFloat($display.data('qty')); + var max = parseFloat($display.data('max')); + var qtyStr = qty.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 2}); + $display.html(''); + var $input = $display.find('input'); + $input.focus().select(); + + var submitQty = function() { + var raw = $input.val().replace(',', '.').trim(); + var newQty = parseFloat(raw); + if (isNaN(newQty) || newQty < 0) { + $display.html(self.formatQty(qty)); + return; + } + // Auf 2 Dezimalstellen runden + newQty = Math.round(newQty * 100) / 100; + if (newQty === qty) { + $display.html(self.formatQty(qty)); + return; + } + if (max > 0 && newQty > max) { + self.showConfirm('Auftragsmenge \u00fcberschritten', 'Auftragsmenge: ' + self.formatQty(max) + '\nNeue Menge: ' + self.formatQty(newQty), 'Trotzdem', 'btn-warning').then(function(ok) { + if (ok) { + self.updateQty(id, newQty); + } else { + $display.html(self.formatQty(qty)); + } + }); + } else { + self.updateQty(id, newQty); + } + }; + + $input.on('blur', submitQty); + $input.on('keydown', function(ev) { + if (ev.key === 'Enter') { ev.preventDefault(); $input.blur(); } + if (ev.key === 'Escape') { $input.off('blur'); $display.html(self.formatQty(qty)); } + }); + }); + // Produkt loeschen $panel.find('.btn-delete-product').on('click', function(e) { e.stopPropagation(); @@ -1060,7 +1132,7 @@ var activeFilter = self.state.productFilter || 'open'; var isDraft = stz && stz.status == 0; - // Merkzettel-Notizen oben anzeigen (wie stundenzettel_commande.php) + // Merkzettel-Notizen oben anzeigen (Abhaken moeglich, neue Notizen nur auf Panel 1) if (self.data.notes && self.data.notes.length) { html += '
'; html += '
'; @@ -1083,25 +1155,6 @@ html += ''; }); html += ''; - // Notiz-Input (nur im Entwurf) - if (isDraft && canWrite) { - html += '
'; - html += ''; - html += ''; - html += '
'; - } - html += '
'; - } else if (isDraft && canWrite) { - // Leere Box mit nur Input zum Hinzufuegen - html += '
'; - html += '
'; - html += '📋'; - html += 'Merkzettel'; - html += '
'; - html += '
'; - html += ''; - html += ''; - html += '
'; html += '
'; } @@ -1295,12 +1348,10 @@ self.showCreateStzDialog(); }); - // Merkzettel auf Panel 2: Abhaken + Hinzufuegen + // Merkzettel auf Panel 2: Abhaken moeglich, neue Notizen nur auf Panel 1 $panel.find('.merkzettel-box .note-checkbox').on('click', function() { self.toggleNote($(this).data('id'), $(this).data('checked')); }); - $('#btn-add-note-p2').on('click', function() { self.addNoteFromPanel2(); }); - $('#note-input-p2').on('keypress', function(e) { if (e.which === 13) self.addNoteFromPanel2(); }); }, renderProductCard: function(p, isDraft, canWrite) { @@ -1336,7 +1387,7 @@ if (isDraft && canWrite) { html += '
'; html += ''; - html += '' + self.formatQty(p.qty_done) + ''; + html += '' + self.formatQty(p.qty_done) + ''; html += ''; html += '
'; } else { @@ -1698,6 +1749,129 @@ }); }, + // ============================================================ + // AKTIONEN: Freigeben / Wiedereroeffnen + // ============================================================ + + validateStz: function() { + var self = this; + + // Pruefen ob Leistungen vorhanden + if (!self.data.leistungen || !self.data.leistungen.length) { + // Keine Leistungen - Warnung mit Option eine hinzuzufuegen + self.showValidateWarningDialog(); + return; + } + + // Leistungen vorhanden - direkt freigeben + self._doValidateStz(); + }, + + showValidateWarningDialog: function() { + var self = this; + var html = ''; + + html += '
'; + html += '

⚠ Keine Leistungen erfasst!

'; + html += '

F\u00fcr die Rechnungsstellung wird mindestens eine Leistungsposition ben\u00f6tigt.

'; + html += '
'; + + html += '
'; + html += '
'; + + // Dienste laden + self.api('get_services', {}).then(function(res) { + if (res.success && res.services) { + var $select = $('#dlg-validate-service'); + res.services.forEach(function(s) { + // Standard-Service nicht doppelt anzeigen + if (s.id == self.state.defaultServiceId) return; + $select.append(''); + }); + } + }); + + var footer = ''; + footer += ''; + footer += ''; + + self.openBottomSheet('Stundenzettel freigeben', html, footer); + + $('#dlg-validate-with-leistung').on('click', function() { + var serviceId = $('#dlg-validate-service').val(); + if (!serviceId) { + self.showToast('Bitte eine Leistung ausw\u00e4hlen', 'error'); + return; + } + self.closeBottomSheet(); + // Leistung mit heutigem Datum und Standard-Zeiten anlegen, dann freigeben + var stz = self.data.stz; + var data = { + stz_id: self.state.stzId, + date: stz.date_iso || new Date().toISOString().substr(0, 10), + time_start: '08:00', + time_end: '16:00', + description: '', + fk_product: serviceId + }; + self.showLoading(); + self.api('add_leistung', data).then(function(res) { + if (res.success) { + self._doValidateStz(); + } else { + self.hideLoading(); + self.showToast(res.error || 'Fehler', 'error'); + } + }); + }); + + $('#dlg-validate-without').on('click', function() { + self.closeBottomSheet(); + self._doValidateStz(); + }); + }, + + _doValidateStz: function() { + var self = this; + self.showLoading(); + self.api('validate_stz', {stz_id: self.state.stzId}).then(function(res) { + self.hideLoading(); + if (res.success) { + self.showToast('Stundenzettel freigegeben', 'success'); + self.reloadData(); + } else { + self.showToast(res.error || 'Fehler beim Freigeben', 'error'); + } + }).catch(function() { + self.hideLoading(); + self.showToast('Verbindungsfehler', 'error'); + }); + }, + + setDraftStz: function() { + var self = this; + self.showLoading(); + self.api('setdraft_stz', {stz_id: self.state.stzId}).then(function(res) { + self.hideLoading(); + if (res.success) { + self.showToast('Stundenzettel zur\u00fcck auf Entwurf', 'success'); + self.reloadData(); + } else { + self.showToast(res.error || 'Fehler', 'error'); + } + }).catch(function() { + self.hideLoading(); + self.showToast('Verbindungsfehler', 'error'); + }); + }, + // ============================================================ // AKTIONEN: Notizen // ============================================================ @@ -1717,21 +1891,6 @@ }); }, - addNoteFromPanel2: function() { - var self = this; - var text = $('#note-input-p2').val().trim(); - if (!text) return; - - self.api('add_note', {stz_id: self.state.stzId, note: text}).then(function(res) { - if (res.success) { - $('#note-input-p2').val(''); - self.reloadData(); - } else { - self.showToast(res.error || 'Fehler', 'error'); - } - }); - }, - toggleNote: function(id, checked) { var self = this; self.api('toggle_note', {note_id: id, checked: checked, stz_id: self.state.stzId}).then(function(res) { diff --git a/pwa.php b/pwa.php index 50ec86e..4ba3cab 100644 --- a/pwa.php +++ b/pwa.php @@ -38,7 +38,7 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#4390dc'); - + @@ -160,7 +160,7 @@ $themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#4390dc'); authUrl: '' }; - +