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 += '';
- } else if (isDraft && canWrite) {
- // Leere Box mit nur Input zum Hinzufuegen
- html += '
';
- 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: ''
};
-
+