/** * Stundenzettel PWA - Haupt-App-Logik * Version 1.0 * * Screens: login -> search -> main (mit 4 Swipe-Panels) * Panels: 0=Alle STZ, 1=Stundenzettel (card.php), 2=Produktliste (Auftrags-Uebernahme), 3=Lieferauflistung */ (function($) { 'use strict'; var App = { // Auth-State auth: { token: null, user: null }, // App-State state: { screen: 'login', orderId: null, orderRef: null, customerName: null, stzId: null, activePanel: 2, // Produktliste als Standard isDragging: false, canWrite: false, canEditStz: false, productFilter: 'open' // Filter: open/done/all (wie Desktop) }, // Daten data: { order: null, stz: null, products: [], leistungen: [], notes: [], tracking: [], stzList: [], orderLines: [], mehraufwandLines: [], services: [] }, // Swipe swipe: { startX: 0, startY: 0, currentX: 0, isDragging: false, startTime: 0, panelWidth: 0 }, // ============================================================ // INITIALISIERUNG // ============================================================ init: function() { var self = this; // Token aus localStorage pruefen var savedToken = localStorage.getItem('stz_pwa_token'); if (savedToken) { self.auth.token = savedToken; self.showLoading(); self.apiAuth('verify', {token: savedToken}).then(function(res) { self.hideLoading(); if (res.success) { self.auth.user = res.user; // Gespeicherten State wiederherstellen (nach Reload) var savedState = self.restoreState(); if (savedState && savedState.orderId) { self.state.activePanel = savedState.activePanel || 2; self.loadOrder(savedState.orderId, savedState.stzId); } else { self.showScreen('search'); } } else { localStorage.removeItem('stz_pwa_token'); self.showScreen('login'); } }).catch(function() { self.hideLoading(); self.showScreen('login'); }); } else { self.showScreen('login'); } self.bindEvents(); }, // State in localStorage speichern (fuer Reload) saveState: function() { if (this.state.orderId) { localStorage.setItem('stz_pwa_state', JSON.stringify({ orderId: this.state.orderId, stzId: this.state.stzId, activePanel: this.state.activePanel })); } }, restoreState: function() { try { var saved = localStorage.getItem('stz_pwa_state'); return saved ? JSON.parse(saved) : null; } catch(e) { return null; } }, // ============================================================ // EVENT-BINDING // ============================================================ bindEvents: function() { var self = this; // Login-Form $('#login-form').on('submit', function(e) { e.preventDefault(); self.handleLogin(); }); // Logout $('#btn-logout').on('click', function() { self.handleLogout(); }); // Suche var searchTimer = null; $('#search-input').on('input', function() { var term = $(this).val().trim(); clearTimeout(searchTimer); if (term.length >= 2) { searchTimer = setTimeout(function() { self.searchCustomers(term); }, 300); } else { $('#search-results').html('

Kundenname eingeben um Aufträge zu finden

'); } }); // Tab-Klicks $('.tab-item').on('click', function() { var panel = parseInt($(this).data('panel')); self.setPanel(panel); }); // Zurueck-Button $('#btn-back').on('click', function() { self.showScreen('search'); }); // FAB $('#fab-add').on('click', function() { self.handleFabClick(); }); // Bottom-Sheet Overlay $('#bottom-sheet-overlay').on('click', function() { self.closeBottomSheet(); }); // Confirm-Dialog Buttons $('#confirm-cancel').on('click', function() { self.closeConfirm(); }); // Swipe-Events self.initSwipe(); }, // ============================================================ // AUTH // ============================================================ handleLogin: function() { var self = this; var username = $('#login-user').val().trim(); var password = $('#login-pass').val(); if (!username || !password) return; $('#login-error').text(''); self.showLoading(); self.apiAuth('login', {username: username, password: password}).then(function(res) { self.hideLoading(); if (res.success) { self.auth.token = res.token; self.auth.user = res.user; localStorage.setItem('stz_pwa_token', res.token); $('#login-user').val(''); $('#login-pass').val(''); self.showScreen('search'); } else { $('#login-error').text(res.error || 'Login fehlgeschlagen'); } }).catch(function() { self.hideLoading(); $('#login-error').text('Verbindungsfehler'); }); }, handleLogout: function() { this.auth.token = null; this.auth.user = null; localStorage.removeItem('stz_pwa_token'); localStorage.removeItem('stz_pwa_state'); this.state.orderId = null; this.state.stzId = null; this.data = {order: null, stz: null, products: [], leistungen: [], notes: [], tracking: [], stzList: [], orderLines: [], mehraufwandLines: [], services: [], leistungenSummary: [], leistungenAll: []}; this.showScreen('login'); }, // ============================================================ // API-HELPER // ============================================================ apiAuth: function(action, data) { data = data || {}; data.action = action; return $.ajax({ url: window.STZ_CONFIG.authUrl, method: 'POST', data: data, dataType: 'json', timeout: 15000 }); }, api: function(action, data) { var self = this; data = data || {}; data.action = action; data.token = self.auth.token; return $.ajax({ url: window.STZ_CONFIG.apiUrl, method: 'POST', data: data, dataType: 'json', timeout: 30000 }).fail(function(xhr) { if (xhr.status === 401) { self.showToast('Sitzung abgelaufen', 'error'); self.handleLogout(); } }); }, // ============================================================ // SCREEN-MANAGEMENT // ============================================================ showScreen: function(name) { $('.screen').removeClass('active'); $('#screen-' + name).addClass('active'); this.state.screen = name; if (name === 'search') { $('#search-input').focus(); } if (name === 'main') { this.updatePanelWidth(); this.setPanel(this.state.activePanel, false); this.updateFab(); } else { $('#fab-add').addClass('hidden'); } }, // ============================================================ // TOASTS // ============================================================ showToast: function(msg, type) { type = type || 'info'; var $toast = $('
' + this.escHtml(msg) + '
'); $('#toast-container').append($toast); setTimeout(function() { $toast.css('animation', 'toastOut 0.3s ease-out forwards'); setTimeout(function() { $toast.remove(); }, 300); }, 3000); }, // ============================================================ // LOADING // ============================================================ showLoading: function() { $('#loading-overlay').addClass('active'); }, hideLoading: function() { $('#loading-overlay').removeClass('active'); }, // ============================================================ // CONFIRM-DIALOG // ============================================================ showConfirm: function(title, text, okText, okClass) { var self = this; $('#confirm-title').text(title); $('#confirm-text').text(text); $('#confirm-ok').text(okText || 'OK').attr('class', 'btn ' + (okClass || 'btn-danger')); $('#confirm-dialog').addClass('active'); return new Promise(function(resolve) { self._confirmResolve = resolve; $('#confirm-ok').off('click').on('click', function() { self.closeConfirm(); resolve(true); }); $('#confirm-cancel').off('click').on('click', function() { self.closeConfirm(); resolve(false); }); }); }, closeConfirm: function() { $('#confirm-dialog').removeClass('active'); }, // ============================================================ // BOTTOM-SHEET // ============================================================ openBottomSheet: function(title, bodyHtml, footerHtml) { $('#bottom-sheet-header').html(this.escHtml(title)); $('#bottom-sheet-body').html(bodyHtml); $('#bottom-sheet-footer').html(footerHtml || ''); $('#bottom-sheet-overlay').addClass('open'); // Kleiner Delay fuer die Animation setTimeout(function() { $('#bottom-sheet').addClass('open'); }, 10); }, closeBottomSheet: function() { $('#bottom-sheet').removeClass('open'); setTimeout(function() { $('#bottom-sheet-overlay').removeClass('open'); $('#bottom-sheet-body').html(''); $('#bottom-sheet-footer').html(''); }, 300); }, // ============================================================ // SUCHE // ============================================================ searchCustomers: function(term) { var self = this; self.api('search_customers', {term: term}).then(function(res) { if (res.success && res.customers) { self.renderCustomerResults(res.customers); } else { $('#search-results').html('

' + self.escHtml(res.error || 'Keine Ergebnisse') + '

'); } }).catch(function() { $('#search-results').html('

Verbindungsfehler

'); }); }, renderCustomerResults: function(customers) { var self = this; if (!customers.length) { $('#search-results').html('

Keine Kunden gefunden

'); return; } var html = ''; customers.forEach(function(c) { html += '
'; html += '
'; html += '' + self.escHtml(c.name) + ''; html += '' + c.order_count + ' Aufträge'; html += '
'; html += '
'; html += '
'; }); $('#search-results').html(html); // Klick auf Kunde -> Auftraege laden $('.customer-header').on('click', function() { var $card = $(this).closest('.customer-card'); var customerId = $card.data('customer-id'); var $orders = $card.find('.customer-orders'); // Toggle if ($orders.hasClass('open')) { $orders.removeClass('open'); return; } // Andere schliessen $('.customer-orders').removeClass('open'); // Auftraege laden self.loadCustomerOrders(customerId, $orders); }); }, loadCustomerOrders: function(customerId, $container) { var self = this; $container.html('
Laden...
'); $container.addClass('open'); self.api('get_customer_orders', {customer_id: customerId}).then(function(res) { if (res.success && res.orders) { var html = ''; if (!res.orders.length) { html = '
Keine Aufträge
'; } else { res.orders.forEach(function(o) { html += '
'; html += '
'; html += '
' + self.escHtml(o.ref) + '
'; if (o.ref_client) { html += '
' + self.escHtml(o.ref_client) + '
'; } html += '
' + self.escHtml(o.date) + '
'; html += '
'; if (o.has_draft_stz) { html += 'Offener STZ'; } html += '
'; }); } $container.html(html); // Klick auf Auftrag $container.find('.order-item').on('click', function() { var orderId = $(this).data('order-id'); self.loadOrder(orderId); }); } }); }, // ============================================================ // AUFTRAG LADEN // ============================================================ loadOrder: function(orderId, stzId, callback) { var self = this; self.showLoading(); var params = {order_id: orderId}; if (stzId) params.stz_id = stzId; self.api('get_order_context', params).then(function(res) { self.hideLoading(); if (res.success) { self.state.orderId = orderId; self.state.orderRef = res.order.ref; 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; self.data.stz = res.stz; } else { self.state.stzId = null; self.data.stz = null; } self.data.order = res.order; self.data.products = res.products || []; self.data.leistungen = res.leistungen || []; self.data.notes = res.notes || []; self.data.tracking = res.tracking || []; self.data.stzList = res.stz_list || []; self.data.orderLines = res.order_lines || []; self.data.mehraufwandLines = res.mehraufwand_lines || []; self.data.leistungenSummary = res.leistungen_summary || []; self.data.leistungenAll = res.leistungen_all || []; // Alle Panels rendern self.renderAllPanels(); self.showScreen('main'); self.saveState(); // Callback ausfuehren (z.B. Panel-Wechsel) if (typeof callback === 'function') { callback(); } // Wenn kein STZ existiert, direkt erstellen anbieten if (!res.stz && !stzId) { self.showCreateStzDialog(); } } else { self.showToast(res.error || 'Fehler beim Laden', 'error'); } }).catch(function() { self.hideLoading(); self.showToast('Verbindungsfehler', 'error'); }); }, // Daten neu laden (nach Aenderungen) reloadData: function() { if (this.state.orderId) { this.loadOrder(this.state.orderId, this.state.stzId); } }, // ============================================================ // SWIPE-ENGINE // ============================================================ initSwipe: function() { var self = this; var container = document.getElementById('swipe-container'); if (!container) return; container.addEventListener('touchstart', function(e) { if (!$('#screen-main').hasClass('active')) return; var touch = e.touches[0]; self.swipe.startX = touch.clientX; self.swipe.startY = touch.clientY; self.swipe.currentX = 0; self.swipe.isDragging = false; self.swipe.startTime = Date.now(); self.swipe.panelWidth = container.parentElement.offsetWidth; container.classList.remove('animating'); }, {passive: true}); container.addEventListener('touchmove', function(e) { if (!$('#screen-main').hasClass('active')) return; var touch = e.touches[0]; var diffX = touch.clientX - self.swipe.startX; var diffY = touch.clientY - self.swipe.startY; // Vertikales Scrollen hat Vorrang if (!self.swipe.isDragging && Math.abs(diffY) > Math.abs(diffX) && Math.abs(diffY) > 10) { return; } if (Math.abs(diffX) > 10) { self.swipe.isDragging = true; } if (self.swipe.isDragging) { self.swipe.currentX = diffX; var baseOffset = -(self.state.activePanel * 25); var dragPercent = (diffX / self.swipe.panelWidth) * 25; // Grenzen: Nicht ueber Panel 0 oder 3 hinaus var newOffset = baseOffset + dragPercent; if (newOffset > 0) newOffset = newOffset * 0.3; // Resistance if (newOffset < -75) newOffset = -75 + (newOffset + 75) * 0.3; container.style.transform = 'translateX(' + newOffset + '%)'; } }, {passive: true}); container.addEventListener('touchend', function(e) { if (!self.swipe.isDragging) return; self.swipe.isDragging = false; var elapsed = Date.now() - self.swipe.startTime; var velocity = Math.abs(self.swipe.currentX) / elapsed; var threshold = self.swipe.panelWidth * 0.25; var newPanel = self.state.activePanel; // Schneller Swipe oder weiter als 25% gezogen if (self.swipe.currentX > threshold || (velocity > 0.5 && self.swipe.currentX > 30)) { newPanel = Math.max(0, self.state.activePanel - 1); } else if (self.swipe.currentX < -threshold || (velocity > 0.5 && self.swipe.currentX < -30)) { newPanel = Math.min(3, self.state.activePanel + 1); } self.setPanel(newPanel, true); }, {passive: true}); }, setPanel: function(index, animate) { var container = document.getElementById('swipe-container'); if (!container) return; if (animate !== false) { container.classList.add('animating'); setTimeout(function() { container.classList.remove('animating'); }, 350); } container.style.transform = 'translateX(' + -(index * 25) + '%)'; this.state.activePanel = index; // Tabs aktualisieren $('.tab-item').removeClass('active'); $('.tab-item[data-panel="' + index + '"]').addClass('active'); // FAB aktualisieren this.updateFab(); // State speichern this.saveState(); // Haptic Feedback if (navigator.vibrate && animate !== false) { navigator.vibrate(10); } }, updatePanelWidth: function() { var viewport = document.querySelector('.swipe-viewport'); if (viewport) { this.swipe.panelWidth = viewport.offsetWidth; } }, // ============================================================ // FAB (Floating Action Button) // ============================================================ updateFab: function() { var $fab = $('#fab-add'); // FAB nur auf Panel 0 (neuen STZ anlegen) - Panel 1 hat eigenen Inline-Button if (this.state.activePanel === 0 && this.state.canWrite) { $fab.removeClass('hidden'); } else { $fab.addClass('hidden'); } }, handleFabClick: function() { if (this.state.activePanel === 0) { this.showCreateStzDialog(); } }, // ============================================================ // PANEL-RENDERING // ============================================================ renderAllPanels: function() { this.renderPanelStzList(); this.renderPanelStundenzettel(); this.renderPanelProducts(); this.renderPanelTracking(); // Scroll-Position aller Panels nach oben zuruecksetzen $('.swipe-panel').scrollTop(0); }, // ---- Panel 0: Alle Stundenzettel ---- renderPanelStzList: function() { var self = this; var html = ''; // Header html += '
'; html += '
'; html += '' + self.escHtml(self.state.orderRef || '') + ''; html += '' + self.escHtml(self.state.customerName || '') + ''; html += '
'; // Freigabe-Hinweis wenn alle STZ freigegeben if (self.allStzReleased()) { html += self.renderReleasedHint(); } html += '
Stundenzettel für diesen Auftrag
'; if (!self.data.stzList.length) { html += '
'; html += '
📋
'; html += '
Noch keine Stundenzettel vorhanden
'; html += '
'; } else { self.data.stzList.forEach(function(s) { var isActive = self.state.stzId && s.id == self.state.stzId; html += '
'; html += '
'; html += '' + self.escHtml(s.ref) + ''; html += '' + self.escHtml(s.status_label) + ''; html += '
'; html += '
' + self.escHtml(s.date) + '
'; html += '
' + s.leistung_count + ' Leistungen, ' + self.escHtml(s.total_hours || '0h') + ' | ' + s.product_count + ' Produkte
'; html += '
'; }); } $('#panel-stzlist').html(html); // Klick auf STZ-Card -> wechseln und zu Panel 1 navigieren $('#panel-stzlist').find('.stz-card').on('click', function() { var stzId = $(this).data('stz-id'); if (stzId != self.state.stzId) { self.loadOrder(self.state.orderId, stzId, function() { self.setPanel(1); }); } else { // Gleicher STZ - nur zu Panel 1 wechseln self.setPanel(1); } }); // Neuen STZ anlegen Button $('#panel-stzlist').find('.btn-create-new-stz').on('click', function() { self.showCreateStzDialog(); }); }, // ---- Panel 1: Stundenzettel (= card.php: Leistungen + Produkte + Mehraufwand + Entfaellt + Ruecknahme + Merkzettel) ---- renderPanelStundenzettel: function() { var self = this; var stz = self.data.stz; var $panel = $('#panel-stundenzettel'); var html = ''; if (!stz) { html += '
'; html += '
📋
'; html += '
Kein Stundenzettel ausgewählt
'; html += '
Wähle im Tab "Alle STZ" einen Stundenzettel oder erstelle einen neuen.
'; html += '
'; $panel.html(html); return; } var isDraft = stz.status == 0; var canWrite = self.state.canEditStz; // STZ-Panel: Nur editierbar wenn Draft + Berechtigung // Header html += '
'; html += '
'; html += '' + self.escHtml(stz.ref) + ''; html += '' + self.escHtml(stz.status_label) + ''; html += '
'; html += '
'; html += '' + self.escHtml(stz.date) + ' · ' + self.escHtml(self.state.customerName || '') + ''; html += '
'; // Hinweis wenn STZ freigegeben if (!isDraft) { html += '
'; html += '🔒'; html += 'Dieser Stundenzettel ist freigegeben – keine Änderungen möglich.'; if (self.state.canWrite) { html += ''; } html += '
'; } // ---- LEISTUNGEN (Accordion) ---- var totalMinutes = 0; self.data.leistungen.forEach(function(l) { totalMinutes += (l.duration_minutes || 0); }); var hasLeistungen = self.data.leistungen.length > 0; if (isDraft || hasLeistungen) { html += '
'; html += 'Gesamt'; html += '' + self.formatDuration(totalMinutes) + ''; html += '
'; // Accordion: Leistungen html += '
'; html += '
'; html += '
'; html += 'Leistungen'; if (hasLeistungen) { html += '' + self.data.leistungen.length + ''; } html += '
'; html += ''; html += '
'; html += '
'; if (!hasLeistungen) { html += '
Noch keine Leistungen erfasst
'; } else { self.data.leistungen.forEach(function(l) { html += '
'; html += '
'; html += '' + self.escHtml(l.time_start) + ' – ' + self.escHtml(l.time_end) + ''; html += '' + self.formatDuration(l.duration_minutes) + ''; html += '
'; if (l.service_name) { html += '
' + self.escHtml(l.service_name) + '
'; } if (l.description) { html += '
' + self.escHtml(l.description) + '
'; } if (isDraft && canWrite) { html += '
'; html += ''; html += ''; html += '
'; } html += '
'; }); } html += '
'; // accordion-body // Button AUSSERHALB accordion-body - immer sichtbar if (isDraft && canWrite) { html += ''; } html += '
'; // accordion-section } // ---- PRODUKTE (Verbaut: origin='order' + 'added') ---- var verbaut = self.data.products.filter(function(p) { return p.origin === 'order' || p.origin === 'added'; }); var mehraufwand = self.data.products.filter(function(p) { return p.origin === 'additional'; }); var entfaellt = self.data.products.filter(function(p) { return p.origin === 'omitted'; }); var ruecknahmen = self.data.products.filter(function(p) { return p.origin === 'returned'; }); if (isDraft || verbaut.length) { html += '
Verbaute Produkte
'; if (!verbaut.length) { html += '
Noch keine Produkte verbaut
'; } else { verbaut.forEach(function(p) { html += self.renderProductCard(p, isDraft, canWrite); }); } // Produkt hinzufuegen Button if (isDraft && canWrite) { html += ''; } } // ---- MEHRAUFWAND (nur anzeigen wenn Inhalt oder Entwurf) ---- if (isDraft || mehraufwand.length) { html += self.renderAccordion('mehraufwand', 'Mehraufwand', mehraufwand, 'additional', isDraft, canWrite); } // ---- ENTFAELLT ---- if (isDraft || entfaellt.length) { html += self.renderAccordion('entfaellt', 'Entfällt', entfaellt, 'omitted', isDraft, canWrite); } // ---- RUECKNAHMEN ---- if (isDraft || ruecknahmen.length) { html += self.renderAccordion('ruecknahmen', 'Rücknahmen', ruecknahmen, 'returned', isDraft, canWrite); } // ---- MERKZETTEL (Accordion, eingeklappt wenn leer) ---- if (isDraft || self.data.notes.length) { var merkzettelOpen = self.data.notes.length > 0; html += '
'; html += '
'; html += '
'; html += 'Merkzettel'; if (self.data.notes.length) { html += '' + self.data.notes.length + ''; } html += '
'; html += ''; html += '
'; html += '
'; if (!self.data.notes.length) { html += '
Keine Notizen
'; } else { self.data.notes.forEach(function(n) { var checked = n.checked == 1; html += '
'; html += ''; html += checked ? '☑' : '☐'; html += ''; html += '' + self.escHtml(n.note) + ''; if (isDraft && canWrite) { html += ''; } html += '
'; }); } html += '
'; // accordion-body // Notiz-Input AUSSERHALB accordion-body - immer sichtbar if (isDraft && canWrite) { html += '
'; html += ''; html += ''; html += '
'; } 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(); self.showEditLeistungDialog($(this).data('id')); }); $panel.find('.btn-delete-leistung').on('click', function(e) { e.stopPropagation(); self.deleteLeistung($(this).data('id')); }); $panel.find('.btn-add-leistung').on('click', function() { self.showAddLeistungDialog(); }); // Produkte +/- $panel.find('.btn-qty-minus').on('click', function(e) { e.stopPropagation(); var qty = parseFloat($(this).data('qty')); if (qty > 0) self.updateQty($(this).data('id'), qty - 1); }); $panel.find('.btn-qty-plus').on('click', function(e) { e.stopPropagation(); var id = $(this).data('id'); var qty = parseFloat($(this).data('qty')); var max = parseFloat($(this).data('max')); var newQty = qty + 1; 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 { self.updateQty(id, newQty); } }); // 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(); var id = $(this).data('id'); var origin = $(this).data('origin'); self.showConfirm('Produkt löschen?', 'Dieses Produkt wirklich vom Stundenzettel entfernen?', 'Löschen').then(function(ok) { if (ok) self.deleteLine(id, origin); }); }); // Produkt hinzufuegen $panel.find('#btn-add-product-inline').on('click', function() { self.showAddProductDialog(); }); // Accordion $panel.find('.accordion-header').on('click', function() { var targetId = $(this).data('target'); $(this).toggleClass('open'); $('#' + targetId).toggleClass('open'); }); // Delete-Buttons in Accordions $panel.find('.btn-delete-line').on('click', function(e) { e.stopPropagation(); var id = $(this).data('id'); var origin = $(this).data('origin'); self.showConfirm('L\u00f6schen?', 'Diesen Eintrag wirklich l\u00f6schen?', 'L\u00f6schen').then(function(ok) { if (ok) self.deleteLine(id, origin); }); }); // Section-Add Buttons $panel.find('.btn-add-section').on('click', function() { var section = $(this).data('section'); switch (section) { case 'mehraufwand': self.showAddMehraufwandDialog(); break; case 'entfaellt': self.showAddEntfaelltDialog(); break; case 'ruecknahmen': self.showAddRuecknahmeDialog(); break; } }); // Merkzettel $panel.find('.note-checkbox').on('click', function() { self.toggleNote($(this).data('id'), $(this).data('checked')); }); $panel.find('.note-delete').on('click', function() { self.deleteNote($(this).data('id')); }); $('#btn-add-note').on('click', function() { self.addNote(); }); $('#note-input').on('keypress', function(e) { if (e.which === 13) self.addNote(); }); // Neuen STZ anlegen (bei freigegebenem STZ) $panel.find('.btn-create-new-stz').on('click', function() { self.showCreateStzDialog(); }); }, // ---- Panel 2: Produktliste (= stundenzettel_commande.php: Auftragspositionen zum Uebernehmen) ---- // Pruefen ob ein Draft-STZ existiert hasDraftStz: function() { for (var i = 0; i < this.data.stzList.length; i++) { if (this.data.stzList[i].status == 0) return true; } return false; }, // Pruefen ob alle STZ freigegeben (status >= 1) sind allStzReleased: function() { if (!this.data.stzList.length) return false; for (var i = 0; i < this.data.stzList.length; i++) { if (this.data.stzList[i].status == 0) return false; } return true; }, // Wiederverwendbarer Hinweis-Block: Alle STZ freigegeben + Neuen anlegen renderReleasedHint: function() { var html = ''; html += '
'; html += '
🔒
'; html += '
Alle Stundenzettel sind freigegeben.
'; html += ''; html += '
'; return html; }, renderPanelProducts: function() { var self = this; var stz = self.data.stz; var $panel = $('#panel-products'); var html = ''; // Info-Header html += '
'; html += '
'; html += '' + self.escHtml(self.state.orderRef || '') + ''; html += 'Produktliste · ' + self.escHtml(self.state.customerName || '') + ''; html += '
'; // STZ-Hinweis: Neuen anlegen wenn keiner da oder alle freigegeben if (!stz || (stz && stz.status != 0)) { if (self.allStzReleased()) { html += self.renderReleasedHint(); } else if (!stz) { html += '
'; html += '
Kein Stundenzettel vorhanden
'; html += ''; html += '
'; } } if (!self.data.orderLines.length) { html += '
'; html += '
📦
'; html += '
Keine Produkte im Auftrag
'; html += '
'; $panel.html(html); // Event-Listener fuer STZ-Anlegen Button $panel.find('.btn-create-new-stz').on('click', function() { self.showCreateStzDialog(); }); return; } var canWrite = self.state.canWrite; var hasSelectable = false; var activeFilter = self.state.productFilter || 'open'; var isDraft = stz && stz.status == 0; // Merkzettel-Notizen oben anzeigen (Abhaken moeglich, neue Notizen nur auf Panel 1) if (self.data.notes && self.data.notes.length) { html += '
'; html += '
'; html += '📋'; html += 'Merkzettel'; if (stz) html += ' (' + self.escHtml(stz.ref) + ')'; html += '
'; html += ''; html += '
'; } // Filter-Buttons (wie Desktop: Offen/Erledigt/Alle) html += '
'; html += ''; html += ''; html += ''; html += '
'; html += '
Auftragspositionen
'; var visibleCount = 0; self.data.orderLines.forEach(function(line) { var isOnStz = line.already_on_stz; var remaining = line.qty_remaining || 0; var delivered = line.qty_delivered || 0; var qtyEffective = line.qty_effective || line.qty; var qtyAdditional = line.qty_additional || 0; var qtyOmitted = line.qty_omitted || 0; var qtyReturned = line.qty_returned || 0; var isDone = (remaining <= 0); var isPartial = delivered > 0 && remaining > 0; // Filter anwenden if (self.state.productFilter === 'open' && isDone) return; if (self.state.productFilter === 'done' && !isDone) return; visibleCount++; html += '
'; html += '
'; // Checkbox immer zeigen ausser wenn bereits auf aktuellem STZ if (canWrite && !isOnStz) { html += ''; hasSelectable = true; } else if (isOnStz) { html += ''; } html += '
'; html += '
' + self.escHtml(line.label || line.description || 'Unbekannt') + '
'; html += '
'; // Status-Badge if (isDone) { html += 'Erledigt'; } else if (isPartial) { html += 'Teilweise'; } else { html += 'Offen'; } html += '
'; // Zahlenzeile: Beauftragt / Verbaut / Verbleibend (wie Desktop) html += '
'; // Beauftragt = effektive Menge mit Aenderungs-Badges (wie Desktop) html += 'Beauftragt: ' + self.formatQty(qtyEffective) + ''; if (qtyAdditional > 0) html += ' +' + self.formatQty(qtyAdditional) + ''; if (qtyOmitted > 0) html += ' -' + self.formatQty(qtyOmitted) + ''; if (qtyReturned > 0) html += ' -' + self.formatQty(qtyReturned) + ''; html += ''; // Verbaut html += 'Verbaut: ' + self.formatQty(delivered) + ''; // Verbleibend html += 'Verbleibend: ' + self.formatQty(remaining) + ''; html += '
'; if (isOnStz) { html += '
Auf aktuellem Stundenzettel
'; } html += '
'; }); // Hinweis wenn Filter keine Ergebnisse liefert if (visibleCount === 0) { if (activeFilter === 'open') { html += '
Alle Produkte erledigt
'; } else if (activeFilter === 'done') { html += '
Noch keine Produkte erledigt
'; } } // Mehraufwand-Sektion (Produkte nicht aus Auftrag, wie Desktop) var mehraufwand = self.data.mehraufwandLines || []; if (mehraufwand.length) { var filteredMA = mehraufwand.filter(function(ma) { if (self.state.productFilter === 'open' && ma.is_done) return false; if (self.state.productFilter === 'done' && !ma.is_done) return false; return true; }); if (filteredMA.length) { html += '
'; html += ' Mehraufwand'; html += ' ' + filteredMA.length + ''; html += '
'; filteredMA.forEach(function(ma) { var isDone = ma.is_done; var remaining = ma.qty_remaining || 0; var done = ma.qty_done || 0; var target = ma.qty_target || 0; var returned = ma.qty_returned || 0; html += '
'; html += '
'; // Checkbox fuer Uebernahme in STZ if (canWrite) { html += ''; hasSelectable = true; } html += '
'; html += '
' + self.escHtml(ma.label || ma.description || 'Unbekannt') + '
'; html += '
'; html += 'Mehraufwand'; html += '
'; // Zahlenzeile: Beauftragt / Verbaut / Verbleibend html += '
'; html += 'Beauftragt: ' + self.formatQty(target) + ''; if (returned > 0) html += ' -' + self.formatQty(returned) + ''; html += ''; html += 'Verbaut: ' + self.formatQty(done) + ''; if (returned > 0) html += ' -' + self.formatQty(returned) + ''; html += ''; html += 'Verbleibend: ' + self.formatQty(remaining) + ''; html += '
'; // STZ-Referenzen if (ma.stz_refs) { html += '
' + self.escHtml(ma.stz_refs) + '
'; } html += '
'; }); } } // Uebernehmen-Button (immer zeigen wenn Schreibrecht - auch ohne Auswahl wird STZ erstellt) if (canWrite) { html += '
'; if (hasSelectable) { html += ''; } html += ''; html += '
'; } $panel.html(html); // Event-Listener $('#select-all-lines').on('change', function() { var checked = $(this).is(':checked'); $panel.find('.order-line-check').prop('checked', checked); }); $('#btn-transfer-products').on('click', function() { // Auftragspositionen (commandedet IDs) var selectedLines = []; $panel.find('.order-line-check:checked').not('.ma-line-check').each(function() { selectedLines.push($(this).data('line-id')); }); // Mehraufwand-Produkte var selectedMA = []; $panel.find('.ma-line-check:checked').each(function() { selectedMA.push({ fk_product: $(this).data('fk-product') || 0, description: $(this).data('description') || '', qty: $(this).data('qty') || 0 }); }); if (!selectedLines.length && !selectedMA.length) { // Keine Produkte ausgewaehlt - nur STZ erstellen self.transferProducts([], []); } else { self.transferProducts(selectedLines, selectedMA); } }); // Filter-Buttons $panel.find('.filter-btn').on('click', function() { var filter = $(this).data('filter'); self.state.productFilter = filter; self.renderPanelProducts(); }); // Neuen STZ anlegen Button $panel.find('.btn-create-new-stz').on('click', function() { self.showCreateStzDialog(); }); // 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')); }); }, renderProductCard: function(p, isDraft, canWrite) { var self = this; var html = ''; html += '
'; html += '
'; html += '
'; html += '
' + self.escHtml(p.label || p.description || 'Unbekannt') + '
'; if (p.ref) { html += '
' + self.escHtml(p.ref) + '
'; } if (!p.ref && p.description && p.label) { html += '
' + self.escHtml(p.description) + '
'; } html += '
'; html += '
'; html += '' + self.getOriginLabel(p.origin) + ''; if (isDraft && canWrite) { html += ''; } html += '
'; html += '
'; html += '
'; if (p.qty_original > 0) { html += 'Auftrag: ' + self.formatQty(p.qty_original) + ''; html += '|'; } html += 'Verbaut:'; if (isDraft && canWrite) { html += '
'; html += ''; html += '' + self.formatQty(p.qty_done) + ''; html += ''; html += '
'; } else { html += '' + self.formatQty(p.qty_done) + ''; } html += '
'; html += '
'; return html; }, renderAccordion: function(id, title, items, origin, isDraft, canWrite) { var self = this; var html = ''; html += '
'; html += '
'; html += '
'; html += '' + title + ''; if (items.length) { html += '' + items.length + ''; } html += '
'; html += ''; html += '
'; html += '
'; if (!items.length) { html += '
Keine Einträge
'; } else { items.forEach(function(p) { html += '
'; html += '
'; html += '
' + self.escHtml(p.label || p.description || 'Unbekannt') + '
'; if (isDraft && canWrite) { html += ''; } html += '
'; html += '
'; html += 'Menge: ' + self.formatQty(p.qty_done) + ''; html += '
'; if (p.description && p.label) { html += '
' + self.escHtml(p.description) + '
'; } html += '
'; }); } html += '
'; // accordion-body // Hinzufuegen-Button AUSSERHALB accordion-body - immer sichtbar if (isDraft && canWrite) { html += ''; } html += '
'; // accordion-section return html; }, // Transfer-Funktion: Auftragspositionen in STZ uebernehmen // Wenn kein Draft-STZ existiert, wird automatisch ein neuer erstellt (wie Desktop) transferProducts: function(lineIds, maProducts) { var self = this; var stz = self.data.stz; var hasDraft = stz && stz.status == 0; maProducts = maProducts || []; var hasAny = lineIds.length || maProducts.length; if (hasDraft) { if (hasAny) { // Draft vorhanden + Produkte ausgewaehlt - direkt uebernehmen self._doTransfer(lineIds, maProducts); } else { // Draft vorhanden aber keine Produkte - zum STZ wechseln self.setPanel(1); self.showToast('Stundenzettel ge\u00f6ffnet', 'success'); } } else { // Kein Draft - neuen STZ anlegen self.showLoading(); var today = new Date().toISOString().substr(0, 10); self.api('create_stundenzettel', { order_id: self.state.orderId, date: today }).then(function(res) { if (res.success) { self.state.stzId = res.stz_id; if (hasAny) { // Produkte ausgewaehlt - uebernehmen self._doTransfer(lineIds, maProducts); } else { // Keine Produkte - nur STZ erstellt, neu laden self.hideLoading(); self.showToast('Stundenzettel erstellt', 'success'); self.loadOrder(self.state.orderId, res.stz_id); } } else { self.hideLoading(); self.showToast(res.error || 'Fehler beim Erstellen', 'error'); } }).catch(function() { self.hideLoading(); self.showToast('Verbindungsfehler', 'error'); }); } }, _doTransfer: function(lineIds, maProducts) { var self = this; self.showLoading(); var params = { stz_id: self.state.stzId, line_ids: lineIds.join(',') }; if (maProducts && maProducts.length) { params.ma_products = JSON.stringify(maProducts); } self.api('transfer_order_products', params).then(function(res) { self.hideLoading(); if (res.success) { self.showToast(res.added + ' Produkte \u00fcbernommen', 'success'); self.reloadData(); } else { self.showToast(res.error || 'Fehler', 'error'); } }).catch(function() { self.hideLoading(); self.showToast('Verbindungsfehler', 'error'); }); }, // ---- Panel 3: Lieferauflistung (wie Desktop: Produkte + Stunden) ---- renderPanelTracking: function() { var self = this; var html = ''; html += '
'; html += '
'; html += '' + self.escHtml(self.state.orderRef || '') + ''; html += 'Lieferauflistung · ' + self.escHtml(self.state.customerName || '') + ''; html += '
'; // ---- PRODUKTE ---- html += '
Lieferfortschritt
'; if (!self.data.tracking.length) { html += '
'; html += '
🚚
'; html += '
Keine Produkte im Auftrag
'; html += '
'; } else { var totalDelivered = 0; self.data.tracking.forEach(function(t) { totalDelivered += t.qty_delivered; html += '
'; html += '
'; html += '
' + self.escHtml(t.label || '') + '
'; html += '' + self.formatQty(t.qty_delivered) + ''; html += '
'; html += '
'; }); // Summenzeile html += '
'; html += 'Gesamt verbaut: ' + self.formatQty(totalDelivered); html += '
'; } // ---- ARBEITSSTUNDEN ---- var leistSummary = self.data.leistungenSummary || []; var leistAll = self.data.leistungenAll || []; if (leistSummary.length || leistAll.length) { html += '
Arbeitsstunden
'; if (leistSummary.length) { // Gesamtstunden berechnen var totalMinAll = 0; leistSummary.forEach(function(ls) { totalMinAll += ls.total_minutes; }); var totalH = Math.floor(totalMinAll / 60); var totalM = totalMinAll % 60; html += '
'; html += 'Gesamt'; html += '' + totalH + ':' + (totalM < 10 ? '0' : '') + totalM + ' h'; html += '
'; // Pro Leistungsposition leistSummary.forEach(function(ls) { html += '
'; html += '
'; html += '
' + self.escHtml(ls.service_label) + '
'; html += '' + self.escHtml(ls.total_hours) + ''; html += '
'; html += '
' + ls.entry_count + ' Eintr\u00e4ge
'; html += '
'; }); } // Einzelne Leistungen (aufklappbar) if (leistAll.length) { html += '
'; html += '
'; html += '
'; html += 'Leistungen pro Stundenzettel'; html += '' + leistAll.length + ''; html += '
'; html += ''; html += '
'; html += '
'; leistAll.forEach(function(l) { html += '
'; html += '
'; html += '' + self.escHtml(l.stz_ref) + ''; html += '' + self.escHtml(l.date) + ''; html += '' + self.escHtml(l.duration) + ''; html += '
'; if (l.time_start && l.time_end) { html += '
' + self.escHtml(l.time_start) + ' - ' + self.escHtml(l.time_end); if (l.service) html += ' · ' + self.escHtml(l.service); html += '
'; } if (l.description) { html += '
' + self.escHtml(l.description) + '
'; } html += '
'; }); html += '
'; html += '
'; } } $('#panel-tracking').html(html); // Accordion $('#panel-tracking').find('.accordion-header').on('click', function() { var targetId = $(this).data('target'); $(this).toggleClass('open'); $('#' + targetId).toggleClass('open'); }); }, // ============================================================ // AKTIONEN: Leistungen // ============================================================ showAddLeistungDialog: function() { var self = this; var stz = self.data.stz; if (!stz) return; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; var footer = ''; self.openBottomSheet('Leistung hinzuf\u00fcgen', html, footer); $('#dlg-leistung-save').on('click', function() { self.saveLeistung(); }); }, showEditLeistungDialog: function(id) { var self = this; var leistung = self.data.leistungen.find(function(l) { return l.id == id; }); if (!leistung) return; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''; var footer = ''; self.openBottomSheet('Leistung bearbeiten', html, footer); $('#dlg-leistung-save').on('click', function() { self.saveLeistung(id); }); }, saveLeistung: function(editId) { var self = this; var data = { stz_id: self.state.stzId, date: $('#dlg-leistung-date').val(), time_start: $('#dlg-leistung-start').val(), time_end: $('#dlg-leistung-end').val(), description: $('#dlg-leistung-desc').val() }; if (!data.date || !data.time_start || !data.time_end) { self.showToast('Bitte alle Zeitfelder ausf\u00fcllen', 'error'); return; } if (data.time_start >= data.time_end) { self.showToast('Endzeit muss nach Startzeit liegen', 'error'); return; } var action = editId ? 'update_leistung' : 'add_leistung'; if (editId) data.leistung_id = editId; self.showLoading(); self.api(action, data).then(function(res) { self.hideLoading(); if (res.success) { self.closeBottomSheet(); self.showToast(editId ? 'Leistung aktualisiert' : 'Leistung hinzugef\u00fcgt', 'success'); self.reloadData(); } else { self.showToast(res.error || 'Fehler', 'error'); } }).catch(function() { self.hideLoading(); self.showToast('Verbindungsfehler', 'error'); }); }, deleteLeistung: function(id) { var self = this; self.showConfirm('Leistung l\u00f6schen?', 'Diese Leistung wirklich l\u00f6schen?', 'L\u00f6schen').then(function(ok) { if (!ok) return; self.showLoading(); self.api('delete_leistung', {leistung_id: id, stz_id: self.state.stzId}).then(function(res) { self.hideLoading(); if (res.success) { self.showToast('Leistung gel\u00f6scht', 'success'); self.reloadData(); } else { self.showToast(res.error || 'Fehler', 'error'); } }); }); }, // ============================================================ // 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 // ============================================================ addNote: function() { var self = this; var text = $('#note-input').val().trim(); if (!text) return; self.api('add_note', {stz_id: self.state.stzId, note: text}).then(function(res) { if (res.success) { $('#note-input').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) { if (res.success) { self.reloadData(); } }); }, deleteNote: function(id) { var self = this; self.api('delete_note', {note_id: id, stz_id: self.state.stzId}).then(function(res) { if (res.success) { self.reloadData(); } }); }, // ============================================================ // AKTIONEN: Produkte // ============================================================ updateQty: function(lineId, newQty) { var self = this; self.api('update_qty', {line_id: lineId, qty_done: newQty, stz_id: self.state.stzId}).then(function(res) { if (res.success) { self.reloadData(); } else { self.showToast(res.error || 'Fehler', 'error'); } }); }, deleteLine: function(lineId, origin) { var self = this; var actionMap = { 'additional': 'delete_mehraufwand', 'omitted': 'delete_entfaellt', 'returned': 'delete_ruecknahme' }; var action = actionMap[origin] || 'delete_product'; self.showLoading(); self.api(action, {line_id: lineId, stz_id: self.state.stzId}).then(function(res) { self.hideLoading(); if (res.success) { self.showToast('Gel\u00f6scht', 'success'); self.reloadData(); } else { self.showToast(res.error || 'Fehler', 'error'); } }); }, showAddProductDialog: function() { var self = this; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''; var footer = ''; self.openBottomSheet('Produkt hinzuf\u00fcgen', html, footer); // Produktsuche var timer = null; $('#dlg-product-search').on('input', function() { var term = $(this).val().trim(); clearTimeout(timer); if (term.length >= 2) { timer = setTimeout(function() { self.searchProducts(term); }, 300); } else { $('#dlg-product-results').html(''); } }); $('#dlg-product-save').on('click', function() { self.saveProduct(); }); }, searchProducts: function(term) { var self = this; self.api('search_products', {term: term}).then(function(res) { if (res.success && res.products) { var html = ''; res.products.forEach(function(p) { html += '
'; html += '
' + self.escHtml(p.label) + '
'; html += '
' + self.escHtml(p.ref) + '
'; html += '
'; }); $('#dlg-product-results').html(html); $('#dlg-product-results .product-card').on('click', function() { var pid = $(this).data('product-id'); var name = $(this).find('.product-name').text(); $('#dlg-product-id').val(pid); $('#dlg-product-search').val(name); $('#dlg-product-freetext').val(''); $('#dlg-product-results').html(''); }); } }); }, saveProduct: function() { var self = this; var productId = $('#dlg-product-id').val(); var freetext = $('#dlg-product-freetext').val().trim(); var qty = parseFloat($('#dlg-product-qty').val()) || 0; if (!productId && !freetext) { self.showToast('Produkt oder Freitext angeben', 'error'); return; } if (qty <= 0) { self.showToast('Menge muss gr\u00f6\u00dfer als 0 sein', 'error'); return; } var data = {stz_id: self.state.stzId, qty: qty}; if (productId) { data.fk_product = productId; } else { data.description = freetext; } self.showLoading(); self.api('add_product', data).then(function(res) { self.hideLoading(); if (res.success) { self.closeBottomSheet(); self.showToast('Produkt hinzugef\u00fcgt', 'success'); self.reloadData(); } else { self.showToast(res.error || 'Fehler', 'error'); } }); }, // ============================================================ // AKTIONEN: Mehraufwand/Entfaellt/Ruecknahme Dialoge // ============================================================ showAddMehraufwandDialog: function() { var self = this; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''; var footer = ''; self.openBottomSheet('Mehraufwand hinzuf\u00fcgen', html, footer); var timer = null; $('#dlg-ma-search').on('input', function() { var term = $(this).val().trim(); clearTimeout(timer); if (term.length >= 2) { timer = setTimeout(function() { self.api('search_products', {term: term}).then(function(res) { if (res.success && res.products) { var rHtml = ''; res.products.forEach(function(p) { rHtml += '
'; rHtml += '
' + self.escHtml(p.label) + '
'; rHtml += '
' + self.escHtml(p.ref) + '
'; rHtml += '
'; }); $('#dlg-ma-results').html(rHtml); $('#dlg-ma-results .product-card').on('click', function() { $('#dlg-ma-product-id').val($(this).data('product-id')); $('#dlg-ma-search').val($(this).find('.product-name').text()); $('#dlg-ma-freetext').val(''); $('#dlg-ma-results').html(''); }); } }); }, 300); } }); $('#dlg-ma-save').on('click', function() { var productId = $('#dlg-ma-product-id').val(); var freetext = $('#dlg-ma-freetext').val().trim(); var qty = parseFloat($('#dlg-ma-qty').val()) || 0; var reason = $('#dlg-ma-reason').val().trim(); if (!productId && !freetext) { self.showToast('Produkt oder Freitext angeben', 'error'); return; } if (qty <= 0) { self.showToast('Menge angeben', 'error'); return; } var data = {stz_id: self.state.stzId, qty: qty, reason: reason}; if (productId) data.fk_product = productId; else data.description = freetext; self.showLoading(); self.api('add_mehraufwand', data).then(function(res) { self.hideLoading(); if (res.success) { self.closeBottomSheet(); self.showToast('Mehraufwand hinzugef\u00fcgt', 'success'); self.reloadData(); } else { self.showToast(res.error || 'Fehler', 'error'); } }); }); }, showAddEntfaelltDialog: function() { var self = this; // Optionen vom Server laden self.showLoading(); self.api('get_entfaellt_options', {stz_id: self.state.stzId, order_id: self.state.orderId}).then(function(res) { self.hideLoading(); if (!res.success) { self.showToast(res.error || 'Fehler', 'error'); return; } var html = ''; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; var footer = ''; self.openBottomSheet('Entf\u00e4llt hinzuf\u00fcgen', html, footer); $('#dlg-ent-product').on('change', function() { var maxQty = $(this).find(':selected').data('max') || 1; $('#dlg-ent-qty').attr('max', maxQty).val(Math.min(parseFloat($('#dlg-ent-qty').val()) || 1, maxQty)); }); $('#dlg-ent-save').on('click', function() { var source = $('#dlg-ent-product').val(); var qty = parseFloat($('#dlg-ent-qty').val()) || 0; var reason = $('#dlg-ent-reason').val().trim(); if (!source) { self.showToast('Produkt w\u00e4hlen', 'error'); return; } if (qty <= 0) { self.showToast('Menge angeben', 'error'); return; } self.showLoading(); self.api('add_entfaellt', {stz_id: self.state.stzId, source: source, qty: qty, reason: reason}).then(function(r) { self.hideLoading(); if (r.success) { self.closeBottomSheet(); self.showToast('Entf\u00e4llt hinzugef\u00fcgt', 'success'); self.reloadData(); } else { self.showToast(r.error || 'Fehler', 'error'); } }); }); }); }, showAddRuecknahmeDialog: function() { var self = this; self.showLoading(); self.api('get_ruecknahme_options', {stz_id: self.state.stzId, order_id: self.state.orderId}).then(function(res) { self.hideLoading(); if (!res.success) { self.showToast(res.error || 'Fehler', 'error'); return; } var html = ''; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; var footer = ''; self.openBottomSheet('R\u00fccknahme hinzuf\u00fcgen', html, footer); $('#dlg-rn-product').on('change', function() { var maxQty = $(this).find(':selected').data('max') || 1; $('#dlg-rn-qty').attr('max', maxQty).val(Math.min(parseFloat($('#dlg-rn-qty').val()) || 1, maxQty)); }); $('#dlg-rn-save').on('click', function() { var source = $('#dlg-rn-product').val(); var qty = parseFloat($('#dlg-rn-qty').val()) || 0; var reason = $('#dlg-rn-reason').val().trim(); if (!source) { self.showToast('Produkt w\u00e4hlen', 'error'); return; } if (qty <= 0) { self.showToast('Menge angeben', 'error'); return; } self.showLoading(); self.api('add_ruecknahme', {stz_id: self.state.stzId, source: source, qty: qty, reason: reason}).then(function(r) { self.hideLoading(); if (r.success) { self.closeBottomSheet(); self.showToast('R\u00fccknahme hinzugef\u00fcgt', 'success'); self.reloadData(); } else { self.showToast(r.error || 'Fehler', 'error'); } }); }); }); }, // ============================================================ // AKTIONEN: Neuen STZ erstellen // ============================================================ showCreateStzDialog: function() { var self = this; var today = new Date().toISOString().split('T')[0]; var html = ''; html += '
'; html += '
'; var footer = ''; self.openBottomSheet('Neuen Stundenzettel anlegen', html, footer); $('#dlg-stz-save').on('click', function() { var date = $('#dlg-stz-date').val(); if (!date) { self.showToast('Datum angeben', 'error'); return; } self.showLoading(); self.api('create_stundenzettel', {order_id: self.state.orderId, date: date}).then(function(res) { self.hideLoading(); if (res.success) { self.closeBottomSheet(); self.showToast('Stundenzettel erstellt', 'success'); self.loadOrder(self.state.orderId, res.stz_id); } else { self.showToast(res.error || 'Fehler', 'error'); } }); }); }, // ============================================================ // HELPER // ============================================================ escHtml: function(str) { if (!str) return ''; var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; }, formatQty: function(qty) { qty = parseFloat(qty) || 0; if (qty === Math.floor(qty)) { return qty.toLocaleString('de-DE', {maximumFractionDigits: 0}); } return qty.toLocaleString('de-DE', {minimumFractionDigits: 1, maximumFractionDigits: 2}); }, formatDuration: function(minutes) { minutes = parseInt(minutes) || 0; var h = Math.floor(minutes / 60); var m = minutes % 60; if (h > 0 && m > 0) return h + 'h ' + m + 'min'; if (h > 0) return h + 'h'; return m + 'min'; }, getStatusClass: function(status) { switch (parseInt(status)) { case 0: return 'draft'; case 1: return 'validated'; case 2: return 'invoiced'; case 9: return 'canceled'; default: return 'draft'; } }, getOriginLabel: function(origin) { switch (origin) { case 'order': return 'Auftrag'; case 'added': return 'Hinzugef\u00fcgt'; case 'additional': return 'Mehraufwand'; case 'omitted': return 'Entf\u00e4llt'; case 'returned': return 'R\u00fccknahme'; default: return origin; } } }; // App starten wenn DOM bereit $(document).ready(function() { App.init(); }); })(jQuery);