// DEBUG FLAG - true für Debug-Ausgaben, false für Produktiv var SUBTOTAL_DEBUG = false; // Fallback für AJAX-URL wenn nicht von PHP gesetzt (z.B. bei direktem JS-Include) if (typeof subtotaltitleAjaxUrl === 'undefined') { // Versuche URL aus aktuellem Script-Pfad abzuleiten var scripts = document.getElementsByTagName('script'); for (var i = 0; i < scripts.length; i++) { var src = scripts[i].src || ''; if (src.indexOf('subtotaltitle') !== -1 && src.indexOf('/js/') !== -1) { // Pfad: .../subtotaltitle/js/subtotaltitle.js -> .../subtotaltitle/ajax/ var subtotaltitleAjaxUrl = src.replace(/\/js\/.*$/, '/ajax/'); break; } } // Letzter Fallback if (typeof subtotaltitleAjaxUrl === 'undefined') { var subtotaltitleAjaxUrl = '/custom/subtotaltitle/ajax/'; } } function debugLog(message) { if (SUBTOTAL_DEBUG) { console.log(message); } } /** * Zeigt einen Dolibarr-styled Bestätigungsdialog * @param {string} title - Dialogtitel * @param {string} content - Dialoginhalt (HTML erlaubt) * @param {function} onConfirm - Callback bei Bestätigung * @param {string} confirmLabel - Text für Bestätigen-Button (optional) * @param {string} cancelLabel - Text für Abbrechen-Button (optional) */ function showConfirmDialog(title, content, onConfirm, confirmLabel, cancelLabel) { confirmLabel = confirmLabel || 'Ja'; cancelLabel = cancelLabel || 'Abbrechen'; var dialogId = 'subtotal-confirm-dialog-' + Date.now(); // Entferne vorherige Dialoge $('.subtotal-confirm-dialog').remove(); var $dialog = $('
', { id: dialogId, 'class': 'subtotal-confirm-dialog', title: title }).appendTo('body'); $dialog.html(content); $dialog.dialog({ autoOpen: true, modal: true, width: 'auto', minWidth: 350, dialogClass: 'confirm-dialog-box', buttons: [ { text: confirmLabel, 'class': 'button-delete', style: 'background:#c00;color:#fff;', click: function() { $(this).dialog('close'); if (typeof onConfirm === 'function') { onConfirm(); } } }, { text: cancelLabel, 'class': 'button-cancel', click: function() { $(this).dialog('close'); } } ], close: function() { $(this).dialog('destroy').remove(); } }); } /** * Zeigt eine Dolibarr-styled Fehlermeldung * @param {string} message - Fehlermeldung */ function showErrorAlert(message) { var dialogId = 'subtotal-error-dialog-' + Date.now(); $('.subtotal-error-dialog').remove(); var $dialog = $('
', { id: dialogId, 'class': 'subtotal-error-dialog', title: 'Fehler' }).appendTo('body'); $dialog.html('
' + message + '
'); $dialog.dialog({ autoOpen: true, modal: true, width: 'auto', minWidth: 300, dialogClass: 'confirm-dialog-box', buttons: [ { text: 'OK', click: function() { $(this).dialog('close'); } } ], close: function() { $(this).dialog('destroy').remove(); } }); } /** * Zeigt einen Dolibarr-styled Eingabedialog * @param {string} title - Dialogtitel * @param {string} label - Label für das Eingabefeld * @param {string} defaultValue - Vorausgefüllter Wert (optional) * @param {function} onConfirm - Callback bei Bestätigung, erhält den eingegebenen Wert * @param {string} confirmLabel - Text für Bestätigen-Button (optional) * @param {string} cancelLabel - Text für Abbrechen-Button (optional) */ function showInputDialog(title, label, defaultValue, onConfirm, confirmLabel, cancelLabel) { confirmLabel = confirmLabel || 'OK'; cancelLabel = cancelLabel || 'Abbrechen'; defaultValue = defaultValue || ''; var dialogId = 'subtotal-input-dialog-' + Date.now(); var inputId = 'subtotal-input-' + Date.now(); // Entferne vorherige Dialoge $('.subtotal-input-dialog').remove(); var $dialog = $('
', { id: dialogId, 'class': 'subtotal-input-dialog', title: title }).appendTo('body'); var content = '
'; content += ''; content += ''; content += '
'; $dialog.html(content); $dialog.dialog({ autoOpen: true, modal: true, width: 400, dialogClass: 'confirm-dialog-box', open: function() { // Fokus auf Eingabefeld setzen var $input = $('#' + inputId); $input.focus().select(); // Enter-Taste zum Bestätigen $input.on('keypress', function(e) { if (e.which === 13) { var value = $(this).val().trim(); if (value) { $dialog.dialog('close'); if (typeof onConfirm === 'function') { onConfirm(value); } } } }); }, buttons: [ { text: confirmLabel, 'class': 'button-save', style: 'background:#0077b3;color:#fff;', click: function() { var value = $('#' + inputId).val().trim(); if (value) { $(this).dialog('close'); if (typeof onConfirm === 'function') { onConfirm(value); } } } }, { text: cancelLabel, 'class': 'button-cancel', click: function() { $(this).dialog('close'); } } ], close: function() { $(this).dialog('destroy').remove(); } }); } /** * Seite neu laden ohne POST-Warnung (GET-Request) */ function safeReload() { var baseUrl = window.location.href.split('?')[0]; var id = getFactureId(); window.location.href = baseUrl + '?id=' + id; } /** * Bereinigt verwaiste Subtotals und fehlerhafte Einträge (NUR aktuelles Dokument!) */ function cleanupOrphanedSubtotals() { var docInfo = getDocumentInfo(); if (!docInfo.id) return; $.post(subtotaltitleAjaxUrl + 'cleanup_subtotals.php', { facture_id: docInfo.id, document_type: docInfo.type }, function(response) { if (response.success) { var total = (response.deleted || 0) + (response.fixed || 0); if (total > 0) { debugLog('🧹 Cleanup (Dokument ' + docInfo.id + '): ' + (response.deleted || 0) + ' verwaiste Subtotals, ' + (response.fixed || 0) + ' fehlerhafte Einträge'); // Kein automatischer Reload - nur im Hintergrund bereinigen } } }, 'json'); } // Flag: Wird gerade gezogen? var isDragging = false; var isTogglingSubtotal = false; // Nur einmal laden! if (typeof SubtotalTitleLoaded === 'undefined') { SubtotalTitleLoaded = true; $(document).ready(function() { debugLog('✅ SubtotalTitle JS loaded!'); // Füge Button zu den Standard-Buttons hinzu - NUR im Entwurfsstatus if ($('#tablelines').length > 0) { // Cleanup verwaister Subtotals (wo show_subtotal=0) cleanupOrphanedSubtotals(); // Prüfe ob Dokument im Entwurfsstatus ist if (typeof subtotalTitleIsDraft !== 'undefined' && subtotalTitleIsDraft === true) { var createBtn = '' + (typeof subtotalTitleLang !== 'undefined' ? subtotalTitleLang.sectionCreate || 'Section erstellen' : 'Section erstellen') + ''; if ($('.tabsAction').length > 0) { $('.tabsAction').prepend(createBtn); debugLog('✅ Section-Button hinzugefügt'); } } else { debugLog('⚠️ Dokument nicht im Entwurfsstatus - Button wird nicht angezeigt'); } initDragAndDrop(); } }); } /** * Holt die Dokument-ID und Typ aus der URL */ function getDocumentInfo() { var url = window.location.href; // Versuche ID aus URL-Parametern zu extrahieren (mehrere Formate) var id = null; // Format 1: ?id=123 oder &id=123 var match = url.match(/[?&]id=(\d+)/); if (match) { id = match[1]; } // Format 2: Aus URLSearchParams (robuster) if (!id) { try { var params = new URLSearchParams(window.location.search); id = params.get('id'); } catch(e) { // URLSearchParams nicht verfügbar in alten Browsern } } // Format 3: Aus dem DOM (falls ID dort gespeichert ist) if (!id) { // Versuche aus versteckten Feldern oder data-Attributen var $idInput = $('input[name="id"], input[name="facid"], input[name="socid"]').first(); if ($idInput.length > 0) { id = $idInput.val(); } } // Format 4: Aus der Seite selbst (Dolibarr zeigt oft die ID an) if (!id) { var $refDiv = $('.refid, .ref').first(); if ($refDiv.length > 0) { var refMatch = $refDiv.text().match(/\((\d+)\)/); if (refMatch) { id = refMatch[1]; } } } // Erkenne Dokumenttyp anhand der URL var docType = 'invoice'; // Default if (url.indexOf('/comm/propal/') !== -1) { docType = 'propal'; } else if (url.indexOf('/commande/') !== -1) { docType = 'order'; } debugLog('getDocumentInfo: id=' + id + ', type=' + docType + ', url=' + url); return { id: id, type: docType }; } /** * DEPRECATED: Verwende getDocumentInfo() stattdessen */ function getFactureId() { return getDocumentInfo().id; } /** * Erstellt eine neue Section */ function createNewSection() { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var docInfo = getDocumentInfo(); if (!docInfo.id) { showErrorAlert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden'); return; } showInputDialog( lang.sectionCreate || 'Positionsgruppe erstellen', lang.sectionName || 'Name der Positionsgruppe:', '', function(title) { debugLog('Erstelle Section: ' + title + ' für ' + docInfo.type + ' ID ' + docInfo.id); $.post(subtotaltitleAjaxUrl + 'create_section.php', { facture_id: docInfo.id, document_type: docInfo.type, title: title }, function(response) { debugLog('Section erstellt: ' + JSON.stringify(response)); if (response.success) { window.location.href = window.location.pathname + window.location.search; } else { showErrorAlert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } }, 'json').fail(function(xhr, status, error) { debugLog('AJAX Fehler: ' + status + ' ' + error); showErrorAlert((lang.errorSavingSection || 'Fehler beim Erstellen') + ': ' + error); }); }, lang.buttonSave || 'Erstellen', lang.buttonCancel || 'Abbrechen' ); } /** * Verschiebt eine Section nach oben/unten */ function moveSection(sectionId, direction) { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var docInfo = getDocumentInfo(); debugLog('🔄 Verschiebe Section ' + sectionId + ' ' + direction + ' (docType: ' + docInfo.type + ')'); $.post(subtotaltitleAjaxUrl + 'move_section.php', { section_id: sectionId, direction: direction, document_type: docInfo.type }, function(response) { debugLog('Move response: ' + JSON.stringify(response)); if (response.success) { window.location.href = window.location.pathname + window.location.search; } else { // "Already at top/bottom" ist kein echter Fehler, sondern eine Info if (response.error === 'Already at top' || response.error === 'Already at bottom') { debugLog('Section ist bereits am ' + (response.error === 'Already at top' ? 'Anfang' : 'Ende')); // Kein Alert nötig - einfach nichts tun } else { alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } } }, 'json').fail(function(xhr, status, error) { if (SUBTOTAL_DEBUG) { console.error('AJAX Fehler: ' + status); console.error('Response Text: ' + xhr.responseText); console.error('Error:', error); } alert((lang.errorReordering || 'Fehler beim Verschieben') + (SUBTOTAL_DEBUG ? '. Siehe Console (F12) für Details.' : '.')); }); } /** * Section umbenennen */ function renameSection(sectionId, currentTitle) { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; showInputDialog( lang.buttonEdit || 'Positionsgruppe umbenennen', lang.sectionName || 'Name der Positionsgruppe:', currentTitle || '', function(newTitle) { debugLog('✏️ Benenne Section ' + sectionId + ' um zu: ' + newTitle); $.post(subtotaltitleAjaxUrl + 'rename_section.php', { section_id: sectionId, title: newTitle }, function(response) { debugLog('Rename response: ' + JSON.stringify(response)); if (response.success) { window.location.href = window.location.pathname + window.location.search; } else { showErrorAlert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } }, 'json').fail(function(xhr, status, error) { debugLog('AJAX Fehler: ' + status + ' ' + error); showErrorAlert((lang.errorSavingSection || 'Fehler beim Umbenennen') + ': ' + error); }); }, lang.buttonSave || 'Speichern', lang.buttonCancel || 'Abbrechen' ); } /** * Löscht eine leere Section */ function deleteSection(sectionId) { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; showConfirmDialog( 'Positionsgruppe löschen', lang.confirmDeleteSection || 'Leere Positionsgruppe löschen?', function() { debugLog('🗑️ Lösche leere Section ' + sectionId); $.post(subtotaltitleAjaxUrl + 'delete_section.php', { section_id: sectionId, force: 0, document_type: getDocumentType() }, function(response) { debugLog('Delete response: ' + JSON.stringify(response)); if (response.success) { window.location.href = window.location.pathname + window.location.search; } else { showErrorAlert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } }, 'json').fail(function(xhr, status, error) { debugLog('AJAX Fehler: ' + status + ' ' + error); showErrorAlert((lang.errorDeletingSection || 'Fehler beim Löschen') + ': ' + error); }); }, 'Ja, löschen', 'Abbrechen' ); } /** * Löscht eine Section MIT allen Produkten (Force-Delete) */ function deleteSectionForce(sectionId) { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var $row = $('tr.section-header[data-section-id="' + sectionId + '"]'); var productCount = $row.data('product-count'); var productIds = $row.data('product-ids') || []; var msg1 = (lang.confirmDeleteSectionForce || 'Wollen Sie wirklich die Positionsgruppe UND alle %s enthaltenen Produkte löschen?').replace('%s', productCount); showConfirmDialog( 'Achtung - Positionsgruppe löschen', '
WARNUNG!

' + msg1 + '

Diese Aktion kann nicht rückgängig gemacht werden!', function() { var msg2 = (lang.confirmDeleteSectionForce2 || 'Sind Sie WIRKLICH sicher? %s Produkte werden unwiderruflich gelöscht!').replace('%s', productCount); showConfirmDialog( 'Letzte Warnung', '
' + msg2 + '
', function() { debugLog('Force-Delete Section ' + sectionId + ' mit Produkten: ' + JSON.stringify(productIds)); $.post(subtotaltitleAjaxUrl + 'delete_section.php', { section_id: sectionId, force: 1, product_ids: JSON.stringify(productIds), document_type: getDocumentType() }, function(response) { debugLog('Force-Delete response: ' + JSON.stringify(response)); if (response.success) { window.location.href = window.location.pathname + window.location.search; } else { showErrorAlert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } }, 'json').fail(function(xhr, status, error) { debugLog('AJAX Fehler: ' + status + ' ' + error); showErrorAlert((lang.errorDeletingSection || 'Fehler beim Löschen') + ': ' + error); }); }, 'Endgültig löschen', 'Abbrechen' ); }, 'Ja, löschen', 'Abbrechen' ); } /** * Fügt leere Sections an die richtige Stelle in der Tabelle ein */ function insertEmptySections() { var docInfo = getDocumentInfo(); if (!docInfo.id) return; debugLog('📦 Lade alle Sections für ' + docInfo.type + ' ID ' + docInfo.id); $.get(subtotaltitleAjaxUrl + 'get_sections.php', { facture_id: docInfo.id, document_type: docInfo.type }, function(response) { if (!response.success) return; debugLog('Gefundene Sections: ' + response.sections.length); // Filtere nur leere Sections var emptySections = response.sections.filter(function(s) { return s.is_empty; }); debugLog('Leere Sections: ' + emptySections.length); // Füge jede leere Section an die richtige Stelle ein emptySections.forEach(function(section) { insertEmptySection(section); }); }, 'json'); } /** * Fügt eine leere Section an die richtige Stelle ein */ function insertEmptySection(section) { if ($('tr.section-header[data-section-id="' + section.id + '"]').length > 0) { debugLog('Section ' + section.title + ' existiert bereits, überspringe'); return; } debugLog('Füge leere Section ein: ' + section.title + ' (order: ' + section.line_order + ')'); var sectionHtml = ''; // Titel (colspan=10) sectionHtml += ''; sectionHtml += ''; sectionHtml += section.title; sectionHtml += '0 Produkte'; // Buttons sectionHtml += ''; sectionHtml += ''; sectionHtml += ''; sectionHtml += ''; // Edit (Spalte 11) sectionHtml += ''; sectionHtml += ''; sectionHtml += ''; sectionHtml += ''; // Delete (Spalte 12) - leere Section = normaler Mülleimer sectionHtml += ''; sectionHtml += ''; sectionHtml += ''; sectionHtml += ''; // Move (Spalte 13) sectionHtml += ''; // Unlink (Spalte 14) sectionHtml += ''; sectionHtml += ''; // Finde die richtige Position var inserted = false; $('#tablelines tbody tr').each(function() { var $row = $(this); if ($row.hasClass('section-empty')) return; var rowOrder = parseInt($row.attr('data-line-order')); if (rowOrder && section.line_order < rowOrder) { $row.before(sectionHtml); inserted = true; debugLog(' → Eingefügt vor Zeile mit order ' + rowOrder); return false; } }); if (!inserted) { // Finde die "Hinzufügen"-Zeile und füge davor ein var $addRow = $('#tablelines tbody tr.liste_titre_create'); if ($addRow.length > 0) { $addRow.before(sectionHtml); debugLog(' → Vor Hinzufügen-Zeile eingefügt'); } else { // Fallback: ans Ende $('#tablelines tbody').append(sectionHtml); debugLog(' → Ans Ende angehängt'); } } } function initDragAndDrop() { debugLog('🖱️ Installiere Drop-Listener...'); setTimeout(function() { debugLog('✅ Überwache Tabellen-Änderungen...'); // Überwache die Tabelle auf Änderungen var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { // Prüfe ob Zeilen verschoben wurden if (mutation.type === 'childList') { // Ignoriere wenn gerade gezogen wird if (isDragging) { debugLog('📦 Tabelle geändert (Drag läuft noch...)'); return; } debugLog('📦 Tabelle geändert - prüfe Reihenfolge...'); // Warte kurz, dann speichere unsere Section-Logik setTimeout(function() { if (!isDragging) { saveCurrentOrder(); } }, 500); } }); }); // Beobachte tbody var tbody = document.querySelector('#tablelines tbody'); if (tbody) { observer.observe(tbody, { childList: true, subtree: false }); debugLog('✅ MutationObserver installiert!'); } }, 2000); } function saveCurrentOrder() { // Nicht speichern wenn gerade Subtotal getoggelt wird if (isTogglingSubtotal) { debugLog('⏸️ Skip saveCurrentOrder - Subtotal Toggle aktiv'); return; } debugLog('💾 Speichere Section-Zuordnungen...'); var updates = []; var order = 1; var currentSectionId = null; $('#tablelines tbody tr').each(function() { var $row = $(this); if ($row.hasClass('liste_titre') || $row.hasClass('liste_titre_add') || $row.hasClass('liste_titre_create') || $row.hasClass('trlinefordates') || $row.attr('id') === 'trlinefordates') { return; } if ($row.hasClass('section-header')) { var sectionId = $row.attr('data-section-id'); if (sectionId) { currentSectionId = sectionId; updates.push({ type: 'section', id: sectionId, order: order }); debugLog(' ' + order + '. 📁 Section #' + sectionId); order++; } } else if ($row.attr('id') && $row.attr('id').indexOf('row-') === 0) { var productId = $row.attr('id').replace('row-', ''); // Prüfe ob Produkt explizit einer Section zugeordnet ist oder frei ist var rowParentSection = $row.attr('data-parent-section'); var assignToSection = null; if (rowParentSection) { // Produkt hat explizite Section-Zuordnung - behalten assignToSection = rowParentSection; } else if (currentSectionId) { // Produkt hat keine Zuordnung - prüfe ob es NACH einem Subtotal steht // Wenn ja, bleibt es frei (am Ende der Liste) var $prevRows = $row.prevAll('tr.subtotal-row[data-section-id="' + currentSectionId + '"]'); if ($prevRows.length === 0) { // Kein Subtotal davor = Produkt gehört zur aktuellen Section assignToSection = currentSectionId; } // Sonst: Subtotal davor = Produkt ist frei (nach der Section) } updates.push({ type: 'product', id: productId, order: order, parent_section: assignToSection }); debugLog(' ' + order + '. 📦 Produkt #' + productId + ' → ' + (assignToSection ? 'Section ' + assignToSection : 'FREI')); order++; } else if ($row.hasClass('textline-row')) { var textlineId = $row.attr('data-textline-id'); if (textlineId) { // Gleiche Logik wie für Produkte var textParentSection = $row.attr('data-parent-section'); var textAssignToSection = null; if (textParentSection) { textAssignToSection = textParentSection; } else if (currentSectionId) { var $prevSubtotals = $row.prevAll('tr.subtotal-row[data-section-id="' + currentSectionId + '"]'); if ($prevSubtotals.length === 0) { textAssignToSection = currentSectionId; } } updates.push({ type: 'text', id: textlineId, order: order, parent_section: textAssignToSection }); debugLog(' ' + order + '. 📝 Text #' + textlineId + ' → ' + (textAssignToSection ? 'Section ' + textAssignToSection : 'FREI')); order++; } } }); if (updates.length === 0) { debugLog('⚠️ Keine Updates gefunden!'); return; } debugLog('🚀 Sende ' + updates.length + ' Updates...'); var docInfo = getDocumentInfo(); $.post(subtotaltitleAjaxUrl + 'reorder_all.php', { facture_id: getFactureId(), document_type: docInfo.type, new_order: JSON.stringify(updates) }, function(response) { debugLog('✅ Server: ' + JSON.stringify(response)); // Kein Reload mehr - Reihenfolge ist gespeichert // if (response.success) { // debugLog('🔄 Reload...'); // window.location.href = window.location.pathname + window.location.search; // } }, 'json').fail(function(xhr) { debugLog('❌ Fehler: ' + xhr.responseText); }); } /** * Verschiebt ein Produkt zu einer Section * MUSS AUSSERHALB sein! */ function moveProductToSection(productId, sectionId, newLineOrder) { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; debugLog('🚀 Verschiebe Produkt ' + productId + ' zu Section ' + (sectionId || 'FREI') + ' auf Position ' + newLineOrder); $.post(subtotaltitleAjaxUrl + 'move_product.php', { product_id: productId, new_section_id: sectionId || 0, new_line_order: newLineOrder }, function(response) { debugLog('Move response: ' + JSON.stringify(response)); if (response.success) { window.location.href = window.location.pathname + window.location.search; } else { alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } }, 'json').fail(function(xhr, status, error) { if (SUBTOTAL_DEBUG) { console.error('AJAX Fehler: ' + status); console.error('Response Text: ' + xhr.responseText); } alert((lang.errorReordering || 'Fehler beim Verschieben') + (SUBTOTAL_DEBUG ? '. Siehe Console (F12) für Details.' : '.')); }); } /** * Fügt allen Zeilen eine "Unlink"-Spalte hinzu */ function addUnlinkColumn() { var $table = $('#tablelines'); if (!$table.length) return; // Prüfe ob schon ausgeführt if ($table.data('unlink-added')) { debugLog('🔗 Unlink-Spalte bereits vorhanden, überspringe'); return; } $table.data('unlink-added', true); debugLog('🔗 Füge Unlink-Spalte hinzu...'); // THEAD: Leere Spalte hinzufügen $table.find('thead tr').each(function() { if ($(this).find('th.linecolunlink').length === 0) { $(this).append(''); } }); // Alle Body-Zeilen durchgehen $table.find('tbody tr').each(function() { var $row = $(this); var rowId = $row.attr('id') || ''; // Prüfe ob Zeile bereits eine Unlink-Spalte hat if ($row.find('td.linecolunlink').length > 0) { return; } // Produkt-Zeilen (row-XXX) if (rowId.match(/^row-\d+/)) { var lineId = rowId.replace('row-', '').split('-')[0]; var parentSection = $row.attr('data-parent-section'); // Robustere Prüfung: Hat gültige Section-ID? var hasValidSection = parentSection && parentSection !== 'null' && parentSection !== 'undefined' && parentSection !== '' && parseInt(parentSection) > 0; debugLog('🔗 Zeile ' + lineId + ': parent_section="' + parentSection + '" → hasValidSection=' + hasValidSection); if (hasValidSection) { // Hat Section → Unlink-Button $row.append(''); } else { // Keine Section → Link-Button (zu passender Section hinzufügen) $row.append(''); } return; } // Alle anderen Zeilen (trlinefordates, liste_titre, etc.): leere Zelle if ($row.find('td').length > 0) { $row.append(''); } }); debugLog('✅ Unlink-Spalte hinzugefügt'); } /** * PLAN B: Section-Assignment beim Produkt hinzufügen */ $(document).ready(function() { // Unlink-Spalte hinzufügen - NACH den PHP-Scripts die data-parent-section setzen // Timeout erhöht auf 1500ms um sicherzustellen dass alle Attribute gesetzt sind setTimeout(addUnlinkColumn, 1500); // 1. Beim Submit des Formulars: Section merken $(document).on('submit', 'form[name="addproduct"]', function(e) { var selectedSection = $('#section_id_dropdown').val(); if (selectedSection) { debugLog('📝 Merke Section ' + selectedSection + ' für nächstes Produkt'); sessionStorage.setItem('pending_section_assignment', selectedSection); sessionStorage.setItem('pending_section_facture', getFactureId()); } }); // 2. Nach Page Reload: Prüfe ob Assignment pending ist var pendingSection = sessionStorage.getItem('pending_section_assignment'); var pendingFacture = sessionStorage.getItem('pending_section_facture'); var currentFacture = getFactureId(); if (pendingSection && pendingFacture == currentFacture) { debugLog('✅ Section-Assignment pending: Section ' + pendingSection); // Entferne aus sessionStorage sessionStorage.removeItem('pending_section_assignment'); sessionStorage.removeItem('pending_section_facture'); // Warte kurz, dann weise zu setTimeout(function() { assignLastProductToSection(pendingSection, currentFacture); }, 1000); } }); /** * Weist das neueste Produkt einer Section zu * Nach der Zuweisung wird die Darstellung per JavaScript aktualisiert (KEIN zweiter Reload) */ function assignLastProductToSection(sectionId, factureId) { var docInfo = getDocumentInfo(); debugLog('🎯 Weise neustes Produkt zu Section ' + sectionId + ' zu (docType: ' + docInfo.type + ')...'); $.post(subtotaltitleAjaxUrl + 'assign_last_product.php', { facture_id: factureId, section_id: sectionId, document_type: docInfo.type }, function(response) { debugLog('✅ Assignment Response: ' + JSON.stringify(response)); if (response.success) { debugLog('✅ Produkt #' + response.product_id + ' zu Section zugewiesen'); // KEIN zweiter Reload - stattdessen Zeile per JavaScript aktualisieren var $productRow = $('#row-' + response.product_id); if ($productRow.length > 0) { // Setze data-parent-section Attribut $productRow.attr('data-parent-section', sectionId); // Verschiebe die Zeile an die richtige Position (nach der Section) var $sectionRow = $('tr.section-header[data-section-id="' + sectionId + '"]'); if ($sectionRow.length > 0) { // Finde das letzte Element dieser Section var $lastInSection = $('tr[data-parent-section="' + sectionId + '"]').last(); if ($lastInSection.length > 0 && $lastInSection[0] !== $productRow[0]) { $lastInSection.after($productRow); } else if ($lastInSection.length === 0) { // Erstes Produkt in der Section $sectionRow.after($productRow); } } // Färbung aktualisieren colorSections(); debugLog('✅ Zeile per JavaScript aktualisiert - kein Reload nötig'); } else { // Fallback: Wenn Zeile nicht gefunden, doch reloaden debugLog('⚠️ Zeile nicht gefunden, Fallback: Reload'); window.location.href = window.location.pathname + window.location.search; } } else { debugLog('❌ Fehler: ' + response.error); } }, 'json').fail(function(xhr, status, error) { debugLog('❌ AJAX Fehler: ' + status); debugLog('Response: ' + xhr.responseText); }); } /** * Ein-/Ausklappen einer Section */ function toggleSection(sectionId) { var $sectionRow = $('tr.section-header[data-section-id="' + sectionId + '"]'); var $toggle = $sectionRow.find('.section-toggle'); var isCollapsed = $sectionRow.hasClass('collapsed'); if (isCollapsed) { // Ausklappen $sectionRow.removeClass('collapsed'); $('tr[data-parent-section="' + sectionId + '"]').removeClass('section-collapsed'); $toggle.text('▼'); saveCollapseState(sectionId, false); debugLog('📂 Section ' + sectionId + ' ausgeklappt'); } else { // Einklappen $sectionRow.addClass('collapsed'); $('tr[data-parent-section="' + sectionId + '"]').addClass('section-collapsed'); $toggle.text('▶'); saveCollapseState(sectionId, true); debugLog('📁 Section ' + sectionId + ' eingeklappt'); } } /** * Alle Sections einklappen */ function collapseAllSections() { $('tr.section-header').each(function() { var sectionId = $(this).attr('data-section-id'); $(this).addClass('collapsed'); $('tr[data-parent-section="' + sectionId + '"]').addClass('section-collapsed'); saveCollapseState(sectionId, true); }); debugLog('📁 Alle Sections eingeklappt'); } /** * Alle Sections ausklappen */ function expandAllSections() { $('tr.section-header').each(function() { var sectionId = $(this).attr('data-section-id'); $(this).removeClass('collapsed'); $('tr[data-parent-section="' + sectionId + '"]').removeClass('section-collapsed'); saveCollapseState(sectionId, false); }); debugLog('📂 Alle Sections ausgeklappt'); } /** * Speichert Collapse-Zustand in localStorage */ function saveCollapseState(sectionId, isCollapsed) { var key = 'section_collapsed_' + sectionId; if (isCollapsed) { localStorage.setItem(key, '1'); debugLog('💾 Gespeichert: ' + key + ' = 1'); } else { localStorage.removeItem(key); debugLog('💾 Gelöscht: ' + key); } } /** * Lädt Collapse-Zustand aus localStorage */ function loadCollapseState(sectionId) { var key = 'section_collapsed_' + sectionId; var value = localStorage.getItem(key); debugLog('📂 Lade: ' + key + ' = ' + value); return value === '1'; } function initCollapse() { debugLog('🔽 Initialisiere Collapse...'); // Aktualisiere Count und lade Zustand für jede Section $('tr.section-header').each(function() { var sectionId = $(this).attr('data-section-id'); var productCount = $('tr[data-parent-section="' + sectionId + '"]').length; // Update count $(this).find('.section-count').text(productCount + ' Produkte'); // Lade gespeicherten Zustand var isCollapsed = loadCollapseState(sectionId); if (isCollapsed) { $(this).addClass('collapsed'); $(this).find('.section-toggle').text('▶'); $('tr[data-parent-section="' + sectionId + '"]').addClass('section-collapsed'); debugLog('Section ' + sectionId + ': ' + productCount + ' Produkte (eingeklappt)'); } else { debugLog('Section ' + sectionId + ': ' + productCount + ' Produkte'); } }); colorSections(); // DEAKTIVIERT: JavaScript-Subtotal verursacht Duplikate // insertLastSectionSubtotal(); debugLog('✅ Collapse initialisiert'); } /** * Prüft und zeigt Subtotal für die LETZTE Section an (wenn aktiviert) * PHP rendert Subtotals nur zwischen Sections, nicht am Ende der Tabelle * Diese Funktion holt die Daten via AJAX und fügt die Zeile mit Checkbox ein */ function insertLastSectionSubtotal() { debugLog('🔢 Prüfe Subtotal für letzte Section...'); var $allSections = $('tr.section-header'); if ($allSections.length === 0) { debugLog(' Keine Sections vorhanden'); return; } // Nur die LETZTE Section prüfen var $lastHeader = $allSections.last(); var sectionId = $lastHeader.attr('data-section-id'); var $checkbox = $lastHeader.find('.subtotal-toggle'); // Nur wenn Checkbox existiert UND aktiviert ist if (!$checkbox.length || !$checkbox.is(':checked')) { debugLog(' Letzte Section ' + sectionId + ': Subtotal nicht aktiviert'); return; } // Finde Produkte dieser Section var $products = $('tr[data-parent-section="' + sectionId + '"]'); if ($products.length === 0) { debugLog(' Letzte Section ' + sectionId + ': Keine Produkte'); return; } var $lastProduct = $products.last(); // Prüfe ob Subtotal für diese Section irgendwo im DOM existiert // (sowohl data-section-id als auch data-subtotal-id prüfen) var $existingSubtotal = $('tr.subtotal-row[data-section-id="' + sectionId + '"]'); if ($existingSubtotal.length > 0) { debugLog(' Letzte Section ' + sectionId + ': Subtotal existiert bereits im DOM ✓'); return; } // Prüfe auch nächste Zeile nach letztem Produkt var $nextRow = $lastProduct.next('tr'); if ($nextRow.hasClass('subtotal-row') || $nextRow.find('td:contains("Zwischensumme")').length > 0) { debugLog(' Letzte Section ' + sectionId + ': Subtotal direkt nach Produkt ✓'); return; } debugLog(' Letzte Section ' + sectionId + ': Subtotal fehlt in DOM, hole Daten...'); // Berechne Summe lokal var sum = 0; $products.each(function() { var priceText = $(this).find('td.linecolht').text().trim(); if (priceText) { var price = parseFloat(priceText.replace(/\s/g, '').replace('.', '').replace(',', '.')); if (!isNaN(price)) { sum += price; } } }); var formattedSum = sum.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); // Hole Subtotal-ID aus Datenbank (oder erstelle ihn falls nötig) var docType = getDocumentType(); $.ajax({ url: subtotaltitleAjaxUrl + 'check_subtotal.php', method: 'GET', data: { section_id: sectionId, document_type: docType }, dataType: 'json', success: function(response) { if (response.exists && response.subtotal_id) { // Subtotal existiert - zeige ihn mit Checkbox an renderSubtotalRow($lastProduct, sectionId, response.subtotal_id, response.in_facturedet, formattedSum); } else { // Subtotal existiert nicht in DB - zeige ohne Checkbox (nur Vorschau) renderSubtotalRow($lastProduct, sectionId, null, false, formattedSum); } }, error: function() { // Bei Fehler: zeige Subtotal ohne Checkbox renderSubtotalRow($lastProduct, sectionId, null, false, formattedSum); } }); } /** * Rendert eine Subtotal-Zeile im DOM */ function renderSubtotalRow($afterElement, sectionId, subtotalId, inFacturedet, formattedSum) { var inClass = inFacturedet ? ' in-facturedet' : ''; var syncChecked = inFacturedet ? 'checked' : ''; var html = ''; html += ' 📄'; } html += ''; html += '' + formattedSum + ' €'; html += ''; html += ''; html += ''; html += ''; html += ''; $afterElement.after(html); debugLog(' → Subtotal eingefügt: ' + formattedSum + ' € (ID: ' + (subtotalId || 'keine') + ')'); } $(document).ready(function() { setTimeout(function() { insertEmptySections(); // ← HIER HINZUFÜGEN! insertTextLines(); initCollapse(); }, 1500); }); /** * Fügt Textzeilen an die richtige Stelle ein */ function insertTextLines() { var docInfo = getDocumentInfo(); if (!docInfo.id) return; debugLog('📝 Lade Textzeilen für ' + docInfo.type + ' ID ' + docInfo.id); $.get(subtotaltitleAjaxUrl + 'get_textlines.php', { facture_id: docInfo.id, document_type: docInfo.type }, function(response) { if (!response.success) return; debugLog('Gefundene Textzeilen: ' + response.textlines.length); response.textlines.forEach(function(textline) { insertTextLine(textline); }); // tableDnD neu initialisieren damit Textzeilen ziehbar sind if (response.textlines.length > 0) { reinitTableDnD(); } }, 'json'); } function toggleSubtotal(sectionId, checkbox) { if (event) event.stopPropagation(); var show = checkbox.checked; var docType = getDocumentType(); debugLog('🔢 Toggle Subtotal für Section ' + sectionId + ': ' + show + ', docType: ' + docType); $.ajax({ url: subtotaltitleAjaxUrl + 'toggle_subtotal.php', method: 'POST', data: { section_id: sectionId, show: show ? 1 : 0, document_type: docType }, dataType: 'json', success: function(response) { debugLog('Subtotal Response: ' + JSON.stringify(response)); if (response.success && response.reload) { safeReload(); } }, error: function(xhr, status, error) { debugLog('Subtotal AJAX Error: ' + error); } }); } /** * Initialisiert tableDnD neu für dynamisch hinzugefügte Zeilen */ function reinitTableDnD() { debugLog('🔄 Reinitialisiere tableDnD...'); var $table = $('#tablelines'); if ($table.length && typeof $.fn.tableDnD !== 'undefined') { // Grip-Hintergrundbild für alle tdlineupdown Elemente setzen (wie Dolibarr es macht) // Das ist nötig für dynamisch hinzugefügte Zeilen // Verwende die von PHP bereitgestellte URL (identisch zu Dolibarr's ajaxrow.tpl.php) if (typeof subtotalTitleGripUrl !== 'undefined') { $(".tdlineupdown").css("background-image", 'url(' + subtotalTitleGripUrl + ')'); $(".tdlineupdown").css("background-repeat", "no-repeat"); $(".tdlineupdown").css("background-position", "center center"); debugLog('🖼️ Grip-Bild gesetzt: ' + subtotalTitleGripUrl); } // Neu initialisieren $table.tableDnD({ onDragClass: 'myDragClass', dragHandle: '.linecolmove', onDragStart: function(table, row) { isDragging = true; debugLog('🎯 Drag gestartet: ' + row.className); }, onDrop: function(table, row) { isDragging = false; debugLog('📦 Drop: ' + row.className); // Kurz warten, dann speichern setTimeout(function() { saveCurrentOrder(); }, 300); } }); // Hover-Effekt für Drag-Handle (wie Dolibarr es macht) $(".tdlineupdown").off("mouseenter mouseleave").hover( function() { $(this).addClass('showDragHandle'); }, function() { $(this).removeClass('showDragHandle'); } ); debugLog('✅ tableDnD neu initialisiert'); } } /** * Fügt eine Textzeile an die richtige Stelle ein */ function insertTextLine(textline) { if ($('tr.textline-row[data-textline-id="' + textline.id + '"]').length > 0) { debugLog('Textzeile ' + textline.id + ' existiert bereits, überspringe'); return; } debugLog('Füge Textzeile ein: ' + textline.title + ' (order: ' + textline.line_order + ')'); var html = ''; // Inhalt (colspan=10) html += ''; html += textline.title; html += ''; // Edit (Spalte 11) html += ''; html += ''; html += ''; html += ''; // Delete (Spalte 12) html += ''; html += ''; html += ''; html += ''; // Move (Spalte 13) html += ''; // Unlink (Spalte 14) html += ''; html += ''; // Finde die richtige Position var inserted = false; $('#tablelines tbody tr').each(function() { var $row = $(this); var rowOrder = parseInt($row.attr('data-line-order')); if (rowOrder && textline.line_order < rowOrder) { $row.before(html); inserted = true; debugLog(' → Eingefügt vor Zeile mit order ' + rowOrder); return false; } }); if (!inserted) { var $addRow = $('#tablelines tbody tr.liste_titre_create'); if ($addRow.length > 0) { $addRow.before(html); debugLog(' → Vor Hinzufügen-Zeile eingefügt'); } else { $('#tablelines tbody').append(html); debugLog(' → Ans Ende angehängt'); } } } /** * Erstellt eine neue Textzeile */ function createTextLine() { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var docInfo = getDocumentInfo(); if (!docInfo.id) { showErrorAlert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden'); return; } showInputDialog( lang.buttonCreateTextline || 'Textzeile erstellen', lang.textlineContent || 'Text eingeben:', '', function(text) { debugLog('Erstelle Textzeile für ' + docInfo.type + ' ID ' + docInfo.id); $.post(subtotaltitleAjaxUrl + 'create_textline.php', { facture_id: docInfo.id, document_type: docInfo.type, text: text }, function(response) { debugLog('Textzeile erstellt: ' + JSON.stringify(response)); if (response.success) { window.location.href = window.location.pathname + window.location.search; } else { showErrorAlert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } }, 'json').fail(function(xhr, status, error) { debugLog('AJAX Fehler: ' + status + ' ' + error); showErrorAlert((lang.errorSavingTextline || 'Fehler beim Erstellen') + ': ' + error); }); }, lang.buttonSave || 'Erstellen', lang.buttonCancel || 'Abbrechen' ); } /** * Textzeile bearbeiten */ function editTextLine(textlineId, currentText) { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; showInputDialog( lang.buttonEdit || 'Textzeile bearbeiten', lang.textlineContent || 'Text:', currentText || '', function(newText) { debugLog('✏️ Bearbeite Textzeile ' + textlineId); $.post(subtotaltitleAjaxUrl + 'edit_textline.php', { textline_id: textlineId, text: newText }, function(response) { debugLog('Edit response: ' + JSON.stringify(response)); if (response.success) { window.location.href = window.location.pathname + window.location.search; } else { showErrorAlert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } }, 'json').fail(function(xhr, status, error) { debugLog('AJAX Fehler: ' + status + ' ' + error); showErrorAlert((lang.errorSavingTextline || 'Fehler beim Bearbeiten') + ': ' + error); }); }, lang.buttonSave || 'Speichern', lang.buttonCancel || 'Abbrechen' ); } /** * Textzeile löschen */ function deleteTextLine(textlineId) { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; showConfirmDialog( 'Textzeile löschen', lang.confirmDeleteTextline || 'Textzeile wirklich löschen?', function() { debugLog('Lösche Textzeile ' + textlineId); $.post(subtotaltitleAjaxUrl + 'delete_textline.php', { textline_id: textlineId, document_type: getDocumentType() }, function(response) { debugLog('Delete response: ' + JSON.stringify(response)); if (response.success) { window.location.href = window.location.pathname + window.location.search; } else { showErrorAlert((lang.errorDeletingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } }, 'json').fail(function(xhr, status, error) { debugLog('AJAX Fehler: ' + status + ' ' + error); showErrorAlert((lang.errorDeletingTextline || 'Fehler beim Löschen') + ': ' + error); }); }, 'Ja, löschen', 'Abbrechen' ); } /** * Entfernt ein Produkt aus seiner Section */ function removeFromSection(productId) { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; showConfirmDialog( 'Aus Positionsgruppe entfernen', lang.confirmRemoveFromSection || 'Produkt aus Positionsgruppe entfernen?', function() { var docType = getDocumentType(); debugLog('Entferne Produkt ' + productId + ' aus Section (docType: ' + docType + ')'); $.post(subtotaltitleAjaxUrl + 'remove_from_section.php', { product_id: productId, document_type: docType }, function(response) { debugLog('Remove response: ' + JSON.stringify(response)); if (response.success) { safeReload(); } else { showErrorAlert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } }, 'json').fail(function(xhr, status, error) { debugLog('AJAX Fehler: ' + status + ' ' + error); showErrorAlert((lang.errorReordering || 'Fehler') + ': ' + error); }); }, 'Ja, entfernen', 'Abbrechen' ); } function toggleMassDelete() { var $checkboxes = $('.mass-delete-checkbox'); if ($checkboxes.length === 0) { // Checkboxen einblenden $('tr.drag[data-line-order]').each(function() { var $row = $(this); var lineId = $row.attr('id')?.match(/row-(\d+)/)?.[1]; if (lineId) { $row.find('td:first').prepend( '' ); } }); // Buttons NACH dem Massenlösch-Button einfügen (Ausgewählte löschen ganz rechts) $('#btnMassDelete').after( 'Abbrechen' + 'Alle auswählen' + 'Ausgewählte löschen' ); // Original-Button verstecken $('#btnMassDelete').hide(); } else { // Checkboxen + Buttons entfernen $('.mass-delete-checkbox').remove(); $('#btnMassDoDelete, #btnMassSelectAll, #btnMassCancel').remove(); $('#btnMassDelete').show(); } } function selectAllLines() { $('.mass-delete-checkbox').prop('checked', true); } function deleteMassSelected() { var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var selectedIds = []; $('.mass-delete-checkbox:checked').each(function() { selectedIds.push($(this).data('line-id')); }); if (selectedIds.length === 0) { showErrorAlert(lang.noLinesSelected || 'Keine Zeilen ausgewählt!'); return; } var msg1 = (lang.confirmDeleteLines || 'Wirklich %s Zeilen löschen?').replace('%s', selectedIds.length); showConfirmDialog('Zeilen löschen', msg1, function() { // Erste Bestätigung OK - zweite Warnung zeigen var msg2 = (lang.confirmDeleteLinesWarning || 'LETZTE WARNUNG: %s Zeilen werden UNWIDERRUFLICH gelöscht!').replace('%s', selectedIds.length); showConfirmDialog('Letzte Warnung', '
' + msg2 + '
', function() { // Zweite Bestätigung OK - jetzt löschen $.post(subtotaltitleAjaxUrl + 'mass_delete.php', { line_ids: JSON.stringify(selectedIds), facture_id: getFactureId(), document_type: getDocumentType() }, function(response) { if (response.success) { safeReload(); } else { showErrorAlert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannt')); } }, 'json'); }, 'Endgültig löschen', 'Abbrechen'); }, 'Ja, löschen', 'Abbrechen'); } function getFactureId() { // Aus URL holen (funktioniert für alle Dokumenttypen) var match = window.location.search.match(/id=(\d+)/); return match ? match[1] : 0; } function getDocumentType() { // Erkenne Dokumenttyp aus URL var path = window.location.pathname; if (path.indexOf('/compta/facture/') > -1 || path.indexOf('/facture/') > -1) { return 'invoice'; } if (path.indexOf('/comm/propal/') > -1 || path.indexOf('/propal/') > -1) { return 'propal'; } if (path.indexOf('/commande/') > -1) { return 'order'; } return 'invoice'; // Fallback } /** * Färbt Sections unterschiedlich ein */ function colorSections() { debugLog('🎨 Färbe Sections ein...'); var colors = ['#4a90d9', '#50b87d', '#e67e22', '#9b59b6', '#e74c3c', '#1abc9c', '#f39c12', '#3498db']; var colorIndex = 0; $('tr.section-header').each(function() { var sectionId = $(this).attr('data-section-id'); var color = colors[colorIndex % colors.length]; // Section-Header färben $(this).find('td:first').css('border-left', '4px solid ' + color); // Alle Produkte dieser Section färben $('tr[data-parent-section="' + sectionId + '"]').each(function() { $(this).find('td:first').css('border-left', '4px solid ' + color); $(this).find('td').css('background-color', hexToRgba(color, 0.05)); }); // Textzeilen dieser Section färben $('tr.textline-row[data-parent-section="' + sectionId + '"]').each(function() { $(this).find('td:first').css('border-left', '4px solid ' + color); }); debugLog(' Section ' + sectionId + ' → ' + color); colorIndex++; }); debugLog('✅ Sections eingefärbt'); } /** * Hex zu RGBA konvertieren */ function hexToRgba(hex, alpha) { var r = parseInt(hex.slice(1, 3), 16); var g = parseInt(hex.slice(3, 5), 16); var b = parseInt(hex.slice(5, 7), 16); return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')'; } /** * Verknüpft ein freies Produkt mit der passenden Section * - Produkte VOR allen Sections → erste Section * - Produkte NACH allen Sections → letzte Section * - Produkte ZWISCHEN Sections → Section in deren Bereich */ function linkToNearestSection(lineId) { debugLog('🔗 linkToNearestSection: line=' + lineId); var docInfo = getDocumentInfo(); if (!docInfo || !docInfo.id) { alert('Fehler: Dokument-Kontext nicht gefunden'); return; } // Hole line_order des Produkts var $productRow = $('tr[id*="' + lineId + '"]'); var productLineOrder = parseInt($productRow.attr('data-line-order')); if (!productLineOrder) { alert('Fehler: line_order nicht gefunden'); return; } debugLog(' Produkt line_order: ' + productLineOrder); // Sammle alle Sections mit ihrer line_order var sections = []; $('tr.section-header').each(function() { var sectionId = $(this).attr('data-section-id'); var sectionLineOrder = parseInt($(this).attr('data-line-order')); var sectionTitle = $(this).attr('data-section-title'); if (sectionId && sectionLineOrder) { sections.push({ id: parseInt(sectionId), lineOrder: sectionLineOrder, title: sectionTitle }); } }); if (sections.length === 0) { alert('Keine Positionsgruppen gefunden'); return; } // Sortiere Sections nach line_order sections.sort(function(a, b) { return a.lineOrder - b.lineOrder; }); debugLog(' Sections: ' + sections.map(function(s) { return s.id + ':' + s.lineOrder; }).join(', ')); // Finde passende Section var targetSection = null; var firstSection = sections[0]; var lastSection = sections[sections.length - 1]; if (productLineOrder < firstSection.lineOrder) { // VOR allen Sections → erste Section targetSection = firstSection; debugLog(' → VOR allen Sections → erste Section #' + targetSection.id); } else if (productLineOrder > lastSection.lineOrder) { // NACH allen Sections → letzte Section targetSection = lastSection; debugLog(' → NACH allen Sections → letzte Section #' + targetSection.id); } else { // ZWISCHEN Sections → finde Section in deren Bereich das Produkt liegt for (var i = 0; i < sections.length - 1; i++) { var currentSection = sections[i]; var nextSection = sections[i + 1]; if (productLineOrder > currentSection.lineOrder && productLineOrder < nextSection.lineOrder) { // Liegt zwischen currentSection und nextSection // Regel: Nimm die Section, die vor dem Produkt liegt targetSection = currentSection; debugLog(' → ZWISCHEN Section #' + currentSection.id + ' und #' + nextSection.id + ' → nehme #' + targetSection.id); break; } } // Falls nicht gefunden (sollte nicht passieren), nimm letzte Section if (!targetSection) { targetSection = lastSection; debugLog(' → Fallback → letzte Section #' + targetSection.id); } } if (!targetSection) { showErrorAlert('Keine passende Positionsgruppe gefunden'); return; } // Bestätigung showConfirmDialog( 'Zur Positionsgruppe hinzufügen', 'Produkt zur Positionsgruppe "' + targetSection.title + '" hinzufügen?', function() { debugLog(' AJAX Call: add_to_section.php'); // AJAX Call zum Backend $.post(subtotaltitleAjaxUrl + 'add_to_section.php', { line_id: lineId, section_id: targetSection.id, document_id: docInfo.id, document_type: docInfo.type }, function(response) { debugLog(' Response: ' + JSON.stringify(response)); if (response.success) { // safeReload statt reload() um POST-Warnung zu vermeiden safeReload(); } else { showErrorAlert('Fehler: ' + (response.error || 'Unbekannter Fehler')); } }, 'json').fail(function(xhr, status, error) { console.error('AJAX Fehler:', status, error); console.error('Response:', xhr.responseText); showErrorAlert('Fehler beim Verknüpfen: ' + xhr.responseText); }); }, 'Ja, hinzufügen', 'Abbrechen' ); }