- 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 <noreply@anthropic.com>
2320 lines
84 KiB
JavaScript
2320 lines
84 KiB
JavaScript
/**
|
|
* 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('<div class="search-empty"><p>Kundenname eingeben um Aufträge zu finden</p></div>');
|
|
}
|
|
});
|
|
|
|
// 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 = $('<div class="toast toast-' + type + '">' + this.escHtml(msg) + '</div>');
|
|
$('#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('<div class="search-empty"><p>' + self.escHtml(res.error || 'Keine Ergebnisse') + '</p></div>');
|
|
}
|
|
}).catch(function() {
|
|
$('#search-results').html('<div class="search-empty"><p>Verbindungsfehler</p></div>');
|
|
});
|
|
},
|
|
|
|
renderCustomerResults: function(customers) {
|
|
var self = this;
|
|
if (!customers.length) {
|
|
$('#search-results').html('<div class="search-empty"><p>Keine Kunden gefunden</p></div>');
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
customers.forEach(function(c) {
|
|
html += '<div class="customer-card" data-customer-id="' + c.id + '">';
|
|
html += '<div class="customer-header">';
|
|
html += '<span class="customer-name">' + self.escHtml(c.name) + '</span>';
|
|
html += '<span class="customer-badge">' + c.order_count + ' Aufträge</span>';
|
|
html += '</div>';
|
|
html += '<div class="customer-orders" id="orders-' + c.id + '"></div>';
|
|
html += '</div>';
|
|
});
|
|
|
|
$('#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('<div style="padding:12px;text-align:center;color:var(--colortextmuted)">Laden...</div>');
|
|
$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 = '<div style="padding:12px;text-align:center;color:var(--colortextmuted)">Keine Aufträge</div>';
|
|
} else {
|
|
res.orders.forEach(function(o) {
|
|
html += '<div class="order-item" data-order-id="' + o.id + '">';
|
|
html += '<div>';
|
|
html += '<div class="order-ref">' + self.escHtml(o.ref) + '</div>';
|
|
if (o.ref_client) {
|
|
html += '<div class="order-client-ref">' + self.escHtml(o.ref_client) + '</div>';
|
|
}
|
|
html += '<div class="order-date">' + self.escHtml(o.date) + '</div>';
|
|
html += '</div>';
|
|
if (o.has_draft_stz) {
|
|
html += '<span class="order-stz-badge">Offener STZ</span>';
|
|
}
|
|
html += '</div>';
|
|
});
|
|
}
|
|
$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 += '<div class="info-header">';
|
|
html += '<div class="info-header-row">';
|
|
html += '<span class="info-value">' + self.escHtml(self.state.orderRef || '') + '</span>';
|
|
html += '<span class="info-label">' + self.escHtml(self.state.customerName || '') + '</span>';
|
|
html += '</div></div>';
|
|
|
|
// Freigabe-Hinweis wenn alle STZ freigegeben
|
|
if (self.allStzReleased()) {
|
|
html += self.renderReleasedHint();
|
|
}
|
|
|
|
html += '<div class="section-header">Stundenzettel für diesen Auftrag</div>';
|
|
|
|
if (!self.data.stzList.length) {
|
|
html += '<div class="empty-state">';
|
|
html += '<div class="empty-state-icon">📋</div>';
|
|
html += '<div class="empty-state-text">Noch keine Stundenzettel vorhanden</div>';
|
|
html += '</div>';
|
|
} else {
|
|
self.data.stzList.forEach(function(s) {
|
|
var isActive = self.state.stzId && s.id == self.state.stzId;
|
|
html += '<div class="stz-card' + (isActive ? ' active-stz' : '') + '" data-stz-id="' + s.id + '">';
|
|
html += '<div class="stz-card-header">';
|
|
html += '<span class="stz-ref">' + self.escHtml(s.ref) + '</span>';
|
|
html += '<span class="badge badge-' + self.getStatusClass(s.status) + '">' + self.escHtml(s.status_label) + '</span>';
|
|
html += '</div>';
|
|
html += '<div class="stz-date">' + self.escHtml(s.date) + '</div>';
|
|
html += '<div class="stz-info">' + s.leistung_count + ' Leistungen, ' + self.escHtml(s.total_hours || '0h') + ' | ' + s.product_count + ' Produkte</div>';
|
|
html += '</div>';
|
|
});
|
|
}
|
|
|
|
$('#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 += '<div class="empty-state">';
|
|
html += '<div class="empty-state-icon">📋</div>';
|
|
html += '<div class="empty-state-text">Kein Stundenzettel ausgewählt</div>';
|
|
html += '<div class="empty-state-text" style="font-size:13px;margin-top:8px;">Wähle im Tab "Alle STZ" einen Stundenzettel oder erstelle einen neuen.</div>';
|
|
html += '</div>';
|
|
$panel.html(html);
|
|
return;
|
|
}
|
|
|
|
var isDraft = stz.status == 0;
|
|
var canWrite = self.state.canEditStz; // STZ-Panel: Nur editierbar wenn Draft + Berechtigung
|
|
|
|
// Header
|
|
html += '<div class="info-header">';
|
|
html += '<div class="info-header-row">';
|
|
html += '<span class="info-value">' + self.escHtml(stz.ref) + '</span>';
|
|
html += '<span class="badge badge-' + self.getStatusClass(stz.status) + '">' + self.escHtml(stz.status_label) + '</span>';
|
|
html += '</div>';
|
|
html += '<div class="info-header-row">';
|
|
html += '<span class="info-label">' + self.escHtml(stz.date) + ' · ' + self.escHtml(self.state.customerName || '') + '</span>';
|
|
html += '</div></div>';
|
|
|
|
// Hinweis wenn STZ freigegeben
|
|
if (!isDraft) {
|
|
html += '<div class="released-hint released-hint-small">';
|
|
html += '<span class="released-hint-icon">🔒</span>';
|
|
html += '<span class="released-hint-text">Dieser Stundenzettel ist freigegeben – keine Änderungen möglich.</span>';
|
|
if (self.state.canWrite) {
|
|
html += '<button class="btn btn-primary btn-small mt-8 btn-create-new-stz">Neuen STZ anlegen</button>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
// ---- 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 += '<div class="leistung-total">';
|
|
html += '<span class="leistung-total-label">Gesamt</span>';
|
|
html += '<span class="leistung-total-value">' + self.formatDuration(totalMinutes) + '</span>';
|
|
html += '</div>';
|
|
|
|
// Accordion: Leistungen
|
|
html += '<div class="accordion-section">';
|
|
html += '<div class="accordion-header' + (hasLeistungen ? ' open' : '') + '" data-target="accordion-leistungen">';
|
|
html += '<div class="section-title">';
|
|
html += '<span>Leistungen</span>';
|
|
if (hasLeistungen) {
|
|
html += '<span class="section-count">' + self.data.leistungen.length + '</span>';
|
|
}
|
|
html += '</div>';
|
|
html += '<span class="chevron">▼</span>';
|
|
html += '</div>';
|
|
|
|
html += '<div class="accordion-body' + (hasLeistungen ? ' open' : '') + '" id="accordion-leistungen">';
|
|
if (!hasLeistungen) {
|
|
html += '<div class="empty-state"><div class="empty-state-text">Noch keine Leistungen erfasst</div></div>';
|
|
} else {
|
|
self.data.leistungen.forEach(function(l) {
|
|
html += '<div class="leistung-card" data-leistung-id="' + l.id + '">';
|
|
html += '<div class="flex items-center justify-between">';
|
|
html += '<span class="leistung-time">' + self.escHtml(l.time_start) + ' – ' + self.escHtml(l.time_end) + '</span>';
|
|
html += '<span class="leistung-duration">' + self.formatDuration(l.duration_minutes) + '</span>';
|
|
html += '</div>';
|
|
if (l.service_name) {
|
|
html += '<div class="leistung-service">' + self.escHtml(l.service_name) + '</div>';
|
|
}
|
|
if (l.description) {
|
|
html += '<div class="leistung-desc">' + self.escHtml(l.description) + '</div>';
|
|
}
|
|
if (isDraft && canWrite) {
|
|
html += '<div class="leistung-actions">';
|
|
html += '<button class="btn btn-ghost btn-small btn-edit-leistung" data-id="' + l.id + '">Bearbeiten</button>';
|
|
html += '<button class="btn btn-danger btn-small btn-delete-leistung" data-id="' + l.id + '">🗑</button>';
|
|
html += '</div>';
|
|
}
|
|
html += '</div>';
|
|
});
|
|
}
|
|
html += '</div>'; // accordion-body
|
|
|
|
// Button AUSSERHALB accordion-body - immer sichtbar
|
|
if (isDraft && canWrite) {
|
|
html += '<button class="btn btn-ghost btn-small w-full mt-8 btn-add-leistung">+ Leistung hinzufügen</button>';
|
|
}
|
|
html += '</div>'; // 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 += '<div class="section-header mt-12">Verbaute Produkte</div>';
|
|
|
|
if (!verbaut.length) {
|
|
html += '<div class="empty-state"><div class="empty-state-text">Noch keine Produkte verbaut</div></div>';
|
|
} else {
|
|
verbaut.forEach(function(p) {
|
|
html += self.renderProductCard(p, isDraft, canWrite);
|
|
});
|
|
}
|
|
|
|
// Produkt hinzufuegen Button
|
|
if (isDraft && canWrite) {
|
|
html += '<button class="btn btn-ghost btn-small w-full mt-8" id="btn-add-product-inline">+ Produkt hinzufügen</button>';
|
|
}
|
|
}
|
|
|
|
// ---- 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 += '<div class="accordion-section mt-12">';
|
|
html += '<div class="accordion-header' + (merkzettelOpen ? ' open' : '') + '" data-target="accordion-merkzettel">';
|
|
html += '<div class="section-title">';
|
|
html += '<span>Merkzettel</span>';
|
|
if (self.data.notes.length) {
|
|
html += '<span class="section-count">' + self.data.notes.length + '</span>';
|
|
}
|
|
html += '</div>';
|
|
html += '<span class="chevron">▼</span>';
|
|
html += '</div>';
|
|
html += '<div class="accordion-body' + (merkzettelOpen ? ' open' : '') + '" id="accordion-merkzettel">';
|
|
|
|
if (!self.data.notes.length) {
|
|
html += '<div class="empty-state"><div class="empty-state-text">Keine Notizen</div></div>';
|
|
} else {
|
|
self.data.notes.forEach(function(n) {
|
|
var checked = n.checked == 1;
|
|
html += '<div class="note-item" data-note-id="' + n.id + '">';
|
|
html += '<span class="note-checkbox' + (checked ? ' checked' : '') + '" data-id="' + n.id + '" data-checked="' + (checked ? 0 : 1) + '">';
|
|
html += checked ? '☑' : '☐';
|
|
html += '</span>';
|
|
html += '<span class="note-text' + (checked ? ' checked' : '') + '">' + self.escHtml(n.note) + '</span>';
|
|
if (isDraft && canWrite) {
|
|
html += '<span class="note-delete" data-id="' + n.id + '">✕</span>';
|
|
}
|
|
html += '</div>';
|
|
});
|
|
}
|
|
html += '</div>'; // accordion-body
|
|
|
|
// Notiz-Input AUSSERHALB accordion-body - immer sichtbar
|
|
if (isDraft && canWrite) {
|
|
html += '<div class="note-add">';
|
|
html += '<input type="text" id="note-input" placeholder="Neue Notiz...">';
|
|
html += '<button class="btn btn-primary btn-icon" id="btn-add-note">+</button>';
|
|
html += '</div>';
|
|
}
|
|
html += '</div>'; // accordion-section
|
|
}
|
|
|
|
// ---- AKTIONS-BUTTONS (Freigeben / Wiedereroeffnen) ----
|
|
if (self.state.canWrite) {
|
|
html += '<div class="stz-actions mt-12">';
|
|
if (isDraft) {
|
|
html += '<button class="btn btn-primary w-full" id="btn-validate-stz">✅ Stundenzettel freigeben</button>';
|
|
} else {
|
|
html += '<button class="btn btn-ghost w-full" id="btn-setdraft-stz">🔓 Zur\u00fcck auf Entwurf</button>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
$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('<input type="text" inputmode="decimal" class="qty-inline-input" value="' + qtyStr + '">');
|
|
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 += '<div class="released-hint">';
|
|
html += '<div class="released-hint-icon">🔒</div>';
|
|
html += '<div class="released-hint-text">Alle Stundenzettel sind freigegeben.</div>';
|
|
html += '<button class="btn btn-primary mt-8 btn-create-new-stz">Neuen Stundenzettel anlegen</button>';
|
|
html += '</div>';
|
|
return html;
|
|
},
|
|
|
|
renderPanelProducts: function() {
|
|
var self = this;
|
|
var stz = self.data.stz;
|
|
var $panel = $('#panel-products');
|
|
var html = '';
|
|
|
|
// Info-Header
|
|
html += '<div class="info-header">';
|
|
html += '<div class="info-header-row">';
|
|
html += '<span class="info-value">' + self.escHtml(self.state.orderRef || '') + '</span>';
|
|
html += '<span class="info-label">Produktliste · ' + self.escHtml(self.state.customerName || '') + '</span>';
|
|
html += '</div></div>';
|
|
|
|
// 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 += '<div class="empty-state">';
|
|
html += '<div class="empty-state-text">Kein Stundenzettel vorhanden</div>';
|
|
html += '<button class="btn btn-primary mt-8 btn-create-new-stz">Neuen Stundenzettel anlegen</button>';
|
|
html += '</div>';
|
|
}
|
|
}
|
|
|
|
if (!self.data.orderLines.length) {
|
|
html += '<div class="empty-state">';
|
|
html += '<div class="empty-state-icon">📦</div>';
|
|
html += '<div class="empty-state-text">Keine Produkte im Auftrag</div>';
|
|
html += '</div>';
|
|
$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 += '<div class="merkzettel-box">';
|
|
html += '<div class="merkzettel-box-header">';
|
|
html += '<span class="merkzettel-box-icon">📋</span>';
|
|
html += '<strong>Merkzettel</strong>';
|
|
if (stz) html += ' <span class="opac">(' + self.escHtml(stz.ref) + ')</span>';
|
|
html += '</div>';
|
|
html += '<ul class="merkzettel-box-list">';
|
|
self.data.notes.forEach(function(n) {
|
|
var checked = n.checked == 1;
|
|
html += '<li class="merkzettel-box-item">';
|
|
if (isDraft && canWrite) {
|
|
html += '<span class="note-checkbox' + (checked ? ' checked' : '') + '" data-id="' + n.id + '" data-checked="' + (checked ? 0 : 1) + '">';
|
|
html += checked ? '☑' : '☐';
|
|
html += '</span>';
|
|
} else {
|
|
html += '<span class="note-checkbox-ro">' + (checked ? '☑' : '☐') + '</span>';
|
|
}
|
|
html += '<span class="' + (checked ? 'note-text checked' : 'note-text') + '">' + self.escHtml(n.note) + '</span>';
|
|
html += '</li>';
|
|
});
|
|
html += '</ul>';
|
|
html += '</div>';
|
|
}
|
|
|
|
// Filter-Buttons (wie Desktop: Offen/Erledigt/Alle)
|
|
html += '<div class="filter-bar">';
|
|
html += '<button class="filter-btn' + (activeFilter === 'open' ? ' active' : '') + '" data-filter="open">Offen</button>';
|
|
html += '<button class="filter-btn' + (activeFilter === 'done' ? ' active' : '') + '" data-filter="done">Erledigt</button>';
|
|
html += '<button class="filter-btn' + (activeFilter === 'all' ? ' active' : '') + '" data-filter="all">Alle</button>';
|
|
html += '</div>';
|
|
|
|
html += '<div class="section-header">Auftragspositionen</div>';
|
|
|
|
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 += '<div class="order-line-card' + (isOnStz ? ' on-stz' : '') + '">';
|
|
html += '<div class="order-line-header">';
|
|
|
|
// Checkbox immer zeigen ausser wenn bereits auf aktuellem STZ
|
|
if (canWrite && !isOnStz) {
|
|
html += '<input type="checkbox" class="order-line-check" data-line-id="' + line.id + '" value="' + line.id + '">';
|
|
hasSelectable = true;
|
|
} else if (isOnStz) {
|
|
html += '<span class="order-line-check-done">✓</span>';
|
|
}
|
|
|
|
html += '<div class="order-line-info">';
|
|
html += '<div class="product-name">' + self.escHtml(line.label || line.description || 'Unbekannt') + '</div>';
|
|
html += '</div>';
|
|
|
|
// Status-Badge
|
|
if (isDone) {
|
|
html += '<span class="badge badge-success">Erledigt</span>';
|
|
} else if (isPartial) {
|
|
html += '<span class="badge badge-warning">Teilweise</span>';
|
|
} else {
|
|
html += '<span class="badge badge-open">Offen</span>';
|
|
}
|
|
html += '</div>';
|
|
|
|
// Zahlenzeile: Beauftragt / Verbaut / Verbleibend (wie Desktop)
|
|
html += '<div class="order-line-numbers">';
|
|
// Beauftragt = effektive Menge mit Aenderungs-Badges (wie Desktop)
|
|
html += '<span>Beauftragt: <strong>' + self.formatQty(qtyEffective) + '</strong>';
|
|
if (qtyAdditional > 0) html += ' <span class="badge badge-info-small" title="Mehraufwand">+' + self.formatQty(qtyAdditional) + '</span>';
|
|
if (qtyOmitted > 0) html += ' <span class="badge badge-danger-small" title="Entf\u00e4llt">-' + self.formatQty(qtyOmitted) + '</span>';
|
|
if (qtyReturned > 0) html += ' <span class="badge badge-danger-small" title="R\u00fccknahme">-' + self.formatQty(qtyReturned) + '</span>';
|
|
html += '</span>';
|
|
// Verbaut
|
|
html += '<span>Verbaut: <strong>' + self.formatQty(delivered) + '</strong></span>';
|
|
// Verbleibend
|
|
html += '<span>Verbleibend: <strong class="' + (remaining > 0 ? 'text-warning' : 'text-success') + '">' + self.formatQty(remaining) + '</strong></span>';
|
|
html += '</div>';
|
|
|
|
if (isOnStz) {
|
|
html += '<div class="order-line-status">Auf aktuellem Stundenzettel</div>';
|
|
}
|
|
html += '</div>';
|
|
});
|
|
|
|
// Hinweis wenn Filter keine Ergebnisse liefert
|
|
if (visibleCount === 0) {
|
|
if (activeFilter === 'open') {
|
|
html += '<div class="empty-state"><div class="empty-state-text">Alle Produkte erledigt</div></div>';
|
|
} else if (activeFilter === 'done') {
|
|
html += '<div class="empty-state"><div class="empty-state-text">Noch keine Produkte erledigt</div></div>';
|
|
}
|
|
}
|
|
|
|
// 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 += '<div class="section-header section-mehraufwand">';
|
|
html += '<span style="color:var(--warning)">⚠</span> Mehraufwand';
|
|
html += ' <span class="section-count">' + filteredMA.length + '</span>';
|
|
html += '</div>';
|
|
|
|
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 += '<div class="order-line-card mehraufwand-card">';
|
|
html += '<div class="order-line-header">';
|
|
|
|
// Checkbox fuer Uebernahme in STZ
|
|
if (canWrite) {
|
|
html += '<input type="checkbox" class="order-line-check ma-line-check" data-fk-product="' + (ma.fk_product || 0) + '" data-description="' + self.escHtml(ma.description || '') + '" data-qty="' + target + '">';
|
|
hasSelectable = true;
|
|
}
|
|
|
|
html += '<div class="order-line-info">';
|
|
html += '<div class="product-name">' + self.escHtml(ma.label || ma.description || 'Unbekannt') + '</div>';
|
|
html += '</div>';
|
|
html += '<span class="badge badge-warning">Mehraufwand</span>';
|
|
html += '</div>';
|
|
|
|
// Zahlenzeile: Beauftragt / Verbaut / Verbleibend
|
|
html += '<div class="order-line-numbers">';
|
|
html += '<span>Beauftragt: <strong>' + self.formatQty(target) + '</strong>';
|
|
if (returned > 0) html += ' <span class="badge badge-danger-small" title="R\u00fccknahme">-' + self.formatQty(returned) + '</span>';
|
|
html += '</span>';
|
|
html += '<span>Verbaut: <strong>' + self.formatQty(done) + '</strong>';
|
|
if (returned > 0) html += ' <span class="badge badge-danger-small" title="R\u00fccknahme">-' + self.formatQty(returned) + '</span>';
|
|
html += '</span>';
|
|
html += '<span>Verbleibend: <strong class="' + (remaining > 0 ? 'text-warning' : 'text-success') + '">' + self.formatQty(remaining) + '</strong></span>';
|
|
html += '</div>';
|
|
|
|
// STZ-Referenzen
|
|
if (ma.stz_refs) {
|
|
html += '<div class="order-line-status">' + self.escHtml(ma.stz_refs) + '</div>';
|
|
}
|
|
html += '</div>';
|
|
});
|
|
}
|
|
}
|
|
|
|
// Uebernehmen-Button (immer zeigen wenn Schreibrecht - auch ohne Auswahl wird STZ erstellt)
|
|
if (canWrite) {
|
|
html += '<div class="transfer-actions">';
|
|
if (hasSelectable) {
|
|
html += '<label class="order-line-select-all"><input type="checkbox" id="select-all-lines"> Alle auswählen</label>';
|
|
}
|
|
html += '<button class="btn btn-ghost w-full mt-8" id="btn-transfer-products">Übernehmen in Stundenzettel</button>';
|
|
html += '</div>';
|
|
}
|
|
|
|
$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 += '<div class="product-card" data-line-id="' + p.id + '">';
|
|
html += '<div class="product-card-header">';
|
|
html += '<div>';
|
|
html += '<div class="product-name">' + self.escHtml(p.label || p.description || 'Unbekannt') + '</div>';
|
|
if (p.ref) {
|
|
html += '<div class="product-ref">' + self.escHtml(p.ref) + '</div>';
|
|
}
|
|
if (!p.ref && p.description && p.label) {
|
|
html += '<div class="product-desc">' + self.escHtml(p.description) + '</div>';
|
|
}
|
|
html += '</div>';
|
|
html += '<div class="product-card-actions">';
|
|
html += '<span class="badge badge-' + p.origin + '">' + self.getOriginLabel(p.origin) + '</span>';
|
|
if (isDraft && canWrite) {
|
|
html += '<button class="btn btn-danger btn-small btn-delete-product" data-id="' + p.id + '" data-origin="' + p.origin + '">🗑</button>';
|
|
}
|
|
html += '</div>';
|
|
html += '</div>';
|
|
|
|
html += '<div class="product-qty-row">';
|
|
if (p.qty_original > 0) {
|
|
html += '<span class="qty-label">Auftrag: <strong>' + self.formatQty(p.qty_original) + '</strong></span>';
|
|
html += '<span style="color:var(--colorborder)">|</span>';
|
|
}
|
|
html += '<span class="qty-label">Verbaut:</span>';
|
|
|
|
if (isDraft && canWrite) {
|
|
html += '<div class="qty-controls">';
|
|
html += '<button class="qty-btn btn-qty-minus" data-id="' + p.id + '" data-qty="' + p.qty_done + '">−</button>';
|
|
html += '<span class="qty-display qty-editable" data-id="' + p.id + '" data-qty="' + p.qty_done + '" data-max="' + (p.qty_original || 9999) + '">' + self.formatQty(p.qty_done) + '</span>';
|
|
html += '<button class="qty-btn btn-qty-plus" data-id="' + p.id + '" data-qty="' + p.qty_done + '" data-max="' + (p.qty_original || 9999) + '">+</button>';
|
|
html += '</div>';
|
|
} else {
|
|
html += '<span class="qty-value">' + self.formatQty(p.qty_done) + '</span>';
|
|
}
|
|
html += '</div>';
|
|
html += '</div>';
|
|
|
|
return html;
|
|
},
|
|
|
|
renderAccordion: function(id, title, items, origin, isDraft, canWrite) {
|
|
var self = this;
|
|
var html = '';
|
|
|
|
html += '<div class="accordion-section">';
|
|
html += '<div class="accordion-header" data-target="accordion-' + id + '">';
|
|
html += '<div class="section-title">';
|
|
html += '<span class="badge badge-' + origin + '">' + title + '</span>';
|
|
if (items.length) {
|
|
html += '<span class="section-count">' + items.length + '</span>';
|
|
}
|
|
html += '</div>';
|
|
html += '<span class="chevron">▼</span>';
|
|
html += '</div>';
|
|
|
|
html += '<div class="accordion-body" id="accordion-' + id + '">';
|
|
|
|
if (!items.length) {
|
|
html += '<div class="empty-state"><div class="empty-state-text">Keine Einträge</div></div>';
|
|
} else {
|
|
items.forEach(function(p) {
|
|
html += '<div class="product-card" data-line-id="' + p.id + '">';
|
|
html += '<div class="product-card-header">';
|
|
html += '<div class="product-name">' + self.escHtml(p.label || p.description || 'Unbekannt') + '</div>';
|
|
if (isDraft && canWrite) {
|
|
html += '<button class="btn btn-danger btn-small btn-delete-line" data-id="' + p.id + '" data-origin="' + origin + '">🗑</button>';
|
|
}
|
|
html += '</div>';
|
|
html += '<div class="product-qty-row">';
|
|
html += '<span class="qty-label">Menge: <strong>' + self.formatQty(p.qty_done) + '</strong></span>';
|
|
html += '</div>';
|
|
if (p.description && p.label) {
|
|
html += '<div class="product-desc">' + self.escHtml(p.description) + '</div>';
|
|
}
|
|
html += '</div>';
|
|
});
|
|
}
|
|
|
|
html += '</div>'; // accordion-body
|
|
|
|
// Hinzufuegen-Button AUSSERHALB accordion-body - immer sichtbar
|
|
if (isDraft && canWrite) {
|
|
html += '<button class="btn btn-ghost btn-small w-full mt-8 btn-add-section" data-section="' + id + '">';
|
|
html += '+ ' + title + ' hinzufügen</button>';
|
|
}
|
|
|
|
html += '</div>'; // 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 += '<div class="info-header">';
|
|
html += '<div class="info-header-row">';
|
|
html += '<span class="info-value">' + self.escHtml(self.state.orderRef || '') + '</span>';
|
|
html += '<span class="info-label">Lieferauflistung · ' + self.escHtml(self.state.customerName || '') + '</span>';
|
|
html += '</div></div>';
|
|
|
|
// ---- PRODUKTE ----
|
|
html += '<div class="section-header">Lieferfortschritt</div>';
|
|
|
|
if (!self.data.tracking.length) {
|
|
html += '<div class="empty-state">';
|
|
html += '<div class="empty-state-icon">🚚</div>';
|
|
html += '<div class="empty-state-text">Keine Produkte im Auftrag</div>';
|
|
html += '</div>';
|
|
} else {
|
|
var totalDelivered = 0;
|
|
|
|
self.data.tracking.forEach(function(t) {
|
|
totalDelivered += t.qty_delivered;
|
|
|
|
html += '<div class="tracking-card">';
|
|
html += '<div class="tracking-card-header">';
|
|
html += '<div class="product-name">' + self.escHtml(t.label || '') + '</div>';
|
|
html += '<span class="tracking-qty">' + self.formatQty(t.qty_delivered) + '</span>';
|
|
html += '</div>';
|
|
html += '</div>';
|
|
});
|
|
|
|
// Summenzeile
|
|
html += '<div class="tracking-total">';
|
|
html += '<strong>Gesamt verbaut:</strong> ' + self.formatQty(totalDelivered);
|
|
html += '</div>';
|
|
}
|
|
|
|
// ---- ARBEITSSTUNDEN ----
|
|
var leistSummary = self.data.leistungenSummary || [];
|
|
var leistAll = self.data.leistungenAll || [];
|
|
|
|
if (leistSummary.length || leistAll.length) {
|
|
html += '<div class="section-header mt-12">Arbeitsstunden</div>';
|
|
|
|
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 += '<div class="leistung-total">';
|
|
html += '<span class="leistung-total-label">Gesamt</span>';
|
|
html += '<span class="leistung-total-value">' + totalH + ':' + (totalM < 10 ? '0' : '') + totalM + ' h</span>';
|
|
html += '</div>';
|
|
|
|
// Pro Leistungsposition
|
|
leistSummary.forEach(function(ls) {
|
|
html += '<div class="tracking-card">';
|
|
html += '<div class="tracking-card-header">';
|
|
html += '<div class="product-name">' + self.escHtml(ls.service_label) + '</div>';
|
|
html += '<span class="leistung-duration">' + self.escHtml(ls.total_hours) + '</span>';
|
|
html += '</div>';
|
|
html += '<div class="product-ref">' + ls.entry_count + ' Eintr\u00e4ge</div>';
|
|
html += '</div>';
|
|
});
|
|
}
|
|
|
|
// Einzelne Leistungen (aufklappbar)
|
|
if (leistAll.length) {
|
|
html += '<div class="accordion-section">';
|
|
html += '<div class="accordion-header" data-target="accordion-leist-detail">';
|
|
html += '<div class="section-title">';
|
|
html += '<span>Leistungen pro Stundenzettel</span>';
|
|
html += '<span class="section-count">' + leistAll.length + '</span>';
|
|
html += '</div>';
|
|
html += '<span class="accordion-arrow">▶</span>';
|
|
html += '</div>';
|
|
html += '<div class="accordion-body" id="accordion-leist-detail">';
|
|
|
|
leistAll.forEach(function(l) {
|
|
html += '<div class="leistung-card-mini">';
|
|
html += '<div class="leistung-mini-header">';
|
|
html += '<span class="badge badge-muted">' + self.escHtml(l.stz_ref) + '</span>';
|
|
html += '<span>' + self.escHtml(l.date) + '</span>';
|
|
html += '<span class="leistung-duration">' + self.escHtml(l.duration) + '</span>';
|
|
html += '</div>';
|
|
if (l.time_start && l.time_end) {
|
|
html += '<div class="leistung-mini-time">' + self.escHtml(l.time_start) + ' - ' + self.escHtml(l.time_end);
|
|
if (l.service) html += ' · ' + self.escHtml(l.service);
|
|
html += '</div>';
|
|
}
|
|
if (l.description) {
|
|
html += '<div class="leistung-mini-desc">' + self.escHtml(l.description) + '</div>';
|
|
}
|
|
html += '</div>';
|
|
});
|
|
|
|
html += '</div>';
|
|
html += '</div>';
|
|
}
|
|
}
|
|
|
|
$('#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 += '<div class="form-group"><label>Datum</label>';
|
|
html += '<input type="date" id="dlg-leistung-date" value="' + (stz.date_iso || '') + '" class="form-group input" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '<div class="flex gap-8">';
|
|
html += '<div class="form-group" style="flex:1"><label>Beginn</label>';
|
|
html += '<input type="time" id="dlg-leistung-start" step="900" value="08:00" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '<div class="form-group" style="flex:1"><label>Ende</label>';
|
|
html += '<input type="time" id="dlg-leistung-end" step="900" value="16:00" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '</div>';
|
|
html += '<div class="form-group"><label>Beschreibung (optional)</label>';
|
|
html += '<textarea id="dlg-leistung-desc" rows="3" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:14px;resize:vertical;"></textarea></div>';
|
|
|
|
var footer = '<button class="btn btn-primary w-full" id="dlg-leistung-save">Leistung hinzufügen</button>';
|
|
|
|
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 += '<div class="form-group"><label>Datum</label>';
|
|
html += '<input type="date" id="dlg-leistung-date" value="' + (leistung.date_iso || '') + '" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '<div class="flex gap-8">';
|
|
html += '<div class="form-group" style="flex:1"><label>Beginn</label>';
|
|
html += '<input type="time" id="dlg-leistung-start" step="900" value="' + self.escHtml(leistung.time_start) + '" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '<div class="form-group" style="flex:1"><label>Ende</label>';
|
|
html += '<input type="time" id="dlg-leistung-end" step="900" value="' + self.escHtml(leistung.time_end) + '" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '</div>';
|
|
html += '<div class="form-group"><label>Beschreibung</label>';
|
|
html += '<textarea id="dlg-leistung-desc" rows="3" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:14px;resize:vertical;">' + self.escHtml(leistung.description || '') + '</textarea></div>';
|
|
html += '<input type="hidden" id="dlg-leistung-id" value="' + id + '">';
|
|
|
|
var footer = '<button class="btn btn-primary w-full" id="dlg-leistung-save">Speichern</button>';
|
|
|
|
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 += '<div class="warning-box mb-12">';
|
|
html += '<p><strong>⚠ Keine Leistungen erfasst!</strong></p>';
|
|
html += '<p>F\u00fcr die Rechnungsstellung wird mindestens eine Leistungsposition ben\u00f6tigt.</p>';
|
|
html += '</div>';
|
|
|
|
html += '<div class="form-group"><label>Leistung ausw\u00e4hlen</label>';
|
|
html += '<select id="dlg-validate-service" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;">';
|
|
html += '<option value="">-- Ohne Leistung freigeben --</option>';
|
|
|
|
// Standard-Service des Kunden vorselektieren
|
|
if (self.state.defaultServiceId > 0) {
|
|
html += '<option value="' + self.state.defaultServiceId + '" selected>' + self.escHtml(self.state.defaultServiceLabel) + ' (Standard)</option>';
|
|
}
|
|
html += '</select></div>';
|
|
|
|
// 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('<option value="' + s.id + '">' + self.escHtml(s.ref + ' - ' + s.label) + '</option>');
|
|
});
|
|
}
|
|
});
|
|
|
|
var footer = '';
|
|
footer += '<button class="btn btn-primary w-full mb-8" id="dlg-validate-with-leistung">Mit Leistung freigeben</button>';
|
|
footer += '<button class="btn btn-ghost w-full" id="dlg-validate-without">Ohne Leistung freigeben</button>';
|
|
|
|
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 += '<div class="form-group"><label>Produkt suchen</label>';
|
|
html += '<input type="search" id="dlg-product-search" placeholder="Ref oder Name..." style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '<div id="dlg-product-results" style="max-height:200px;overflow-y:auto;margin-bottom:12px;"></div>';
|
|
html += '<div class="form-group"><label>Oder Freitext</label>';
|
|
html += '<input type="text" id="dlg-product-freetext" placeholder="Beschreibung..." style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:14px;min-height:48px;"></div>';
|
|
html += '<div class="form-group"><label>Menge</label>';
|
|
html += '<input type="number" id="dlg-product-qty" value="1" min="0.01" step="0.01" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '<input type="hidden" id="dlg-product-id" value="">';
|
|
|
|
var footer = '<button class="btn btn-primary w-full" id="dlg-product-save">Produkt hinzufügen</button>';
|
|
|
|
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 += '<div class="product-card" style="cursor:pointer;margin-bottom:4px" data-product-id="' + p.id + '">';
|
|
html += '<div class="product-name">' + self.escHtml(p.label) + '</div>';
|
|
html += '<div class="product-ref">' + self.escHtml(p.ref) + '</div>';
|
|
html += '</div>';
|
|
});
|
|
$('#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 += '<div class="form-group"><label>Produkt suchen</label>';
|
|
html += '<input type="search" id="dlg-ma-search" placeholder="Ref oder Name..." style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '<div id="dlg-ma-results" style="max-height:200px;overflow-y:auto;margin-bottom:12px;"></div>';
|
|
html += '<div class="form-group"><label>Oder Freitext</label>';
|
|
html += '<input type="text" id="dlg-ma-freetext" placeholder="Beschreibung..." style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:14px;min-height:48px;"></div>';
|
|
html += '<div class="form-group"><label>Menge</label>';
|
|
html += '<input type="number" id="dlg-ma-qty" value="1" min="0.01" step="0.01" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '<div class="form-group"><label>Grund (optional)</label>';
|
|
html += '<input type="text" id="dlg-ma-reason" placeholder="Grund..." style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:14px;min-height:48px;"></div>';
|
|
html += '<input type="hidden" id="dlg-ma-product-id" value="">';
|
|
|
|
var footer = '<button class="btn btn-primary w-full" id="dlg-ma-save">Mehraufwand hinzufügen</button>';
|
|
|
|
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 += '<div class="product-card" style="cursor:pointer;margin-bottom:4px" data-product-id="' + p.id + '">';
|
|
rHtml += '<div class="product-name">' + self.escHtml(p.label) + '</div>';
|
|
rHtml += '<div class="product-ref">' + self.escHtml(p.ref) + '</div>';
|
|
rHtml += '</div>';
|
|
});
|
|
$('#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 += '<div class="form-group"><label>Produkt</label>';
|
|
html += '<select id="dlg-ent-product" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:14px;min-height:48px;">';
|
|
html += '<option value="">-- Bitte wählen --</option>';
|
|
if (res.options && res.options.length) {
|
|
res.options.forEach(function(o) {
|
|
html += '<option value="' + o.value + '" data-max="' + o.max_qty + '">' + self.escHtml(o.label) + ' (max. ' + self.formatQty(o.max_qty) + ')</option>';
|
|
});
|
|
}
|
|
html += '</select></div>';
|
|
html += '<div class="form-group"><label>Menge</label>';
|
|
html += '<input type="number" id="dlg-ent-qty" value="1" min="0.01" step="0.01" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '<div class="form-group"><label>Grund (optional)</label>';
|
|
html += '<input type="text" id="dlg-ent-reason" placeholder="Grund..." style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:14px;min-height:48px;"></div>';
|
|
|
|
var footer = '<button class="btn btn-primary w-full" id="dlg-ent-save">Entfällt hinzufügen</button>';
|
|
|
|
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 += '<div class="form-group"><label>Verbautes Produkt</label>';
|
|
html += '<select id="dlg-rn-product" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:14px;min-height:48px;">';
|
|
html += '<option value="">-- Bitte wählen --</option>';
|
|
if (res.options && res.options.length) {
|
|
res.options.forEach(function(o) {
|
|
html += '<option value="' + o.value + '" data-max="' + o.max_qty + '">' + self.escHtml(o.label) + ' (max. ' + self.formatQty(o.max_qty) + ')</option>';
|
|
});
|
|
}
|
|
html += '</select></div>';
|
|
html += '<div class="form-group"><label>Menge</label>';
|
|
html += '<input type="number" id="dlg-rn-qty" value="1" min="0.01" step="0.01" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
html += '<div class="form-group"><label>Grund (optional)</label>';
|
|
html += '<input type="text" id="dlg-rn-reason" placeholder="Grund..." style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:14px;min-height:48px;"></div>';
|
|
|
|
var footer = '<button class="btn btn-primary w-full" id="dlg-rn-save">Rücknahme hinzufügen</button>';
|
|
|
|
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 += '<div class="form-group"><label>Datum</label>';
|
|
html += '<input type="date" id="dlg-stz-date" value="' + today + '" style="width:100%;padding:12px;background:var(--colorbackinput);border:1px solid var(--colorborder);border-radius:8px;color:var(--colortext);font-size:16px;min-height:48px;"></div>';
|
|
|
|
var footer = '<button class="btn btn-primary w-full" id="dlg-stz-save">Stundenzettel anlegen</button>';
|
|
|
|
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);
|