');
});
},
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;
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');
var stz = this.data.stz;
var isDraft = stz && stz.status == 0;
// FAB auf Panel 0 (neuen STZ anlegen) und Panel 1 (Leistung hinzufuegen, nur Draft)
if (this.state.activePanel === 0 && this.state.canWrite) {
$fab.removeClass('hidden');
} else if (this.state.activePanel === 1 && isDraft && this.state.canEditStz) {
$fab.removeClass('hidden');
} else {
$fab.addClass('hidden');
}
},
handleFabClick: function() {
switch (this.state.activePanel) {
case 0:
this.showCreateStzDialog();
break;
case 1:
this.showAddLeistungDialog();
break;
}
},
// ============================================================
// PANEL-RENDERING
// ============================================================
renderAllPanels: function() {
this.renderPanelStzList();
this.renderPanelStundenzettel();
this.renderPanelProducts();
this.renderPanelTracking();
},
// ---- 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 += '
';
} 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
}
$panel.html(html);
// ---- Event-Listener ----
// 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);
}
});
// 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';
// 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 += '
';
// 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 += '
';
}
if (!p.ref && p.description && p.label) {
html += '
' + self.escHtml(p.description) + '
';
}
html += '
';
html += '' + self.getOriginLabel(p.origin) + '';
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 += '