subtotaltitle/js/subtotaltitle.js

1309 lines
47 KiB
JavaScript
Executable file

// DEBUG FLAG - true für Debug-Ausgaben, false für Produktiv
var SUBTOTAL_DEBUG = false;
function debugLog(message) {
if (SUBTOTAL_DEBUG) {
console.log(message);
}
}
/**
* 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('/dolibarr/custom/subtotaltitle/ajax/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 = '<a class="butAction" href="#" onclick="createNewSection(); return false;">' + (typeof subtotalTitleLang !== 'undefined' ? subtotalTitleLang.sectionCreate || 'Section erstellen' : 'Section erstellen') + '</a>';
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;
var match = url.match(/[?&]id=(\d+)/);
var id = match ? match[1] : null;
// 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';
}
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 title = prompt(lang.sectionName || 'Neue Positionsgruppe - Name eingeben:');
if (!title) return;
var docInfo = getDocumentInfo();
if (!docInfo.id) {
alert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden');
return;
}
debugLog('Erstelle Section: ' + title + ' für ' + docInfo.type + ' ID ' + docInfo.id);
$.post('/dolibarr/custom/subtotaltitle/ajax/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 {
alert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSavingSection || 'Fehler beim Erstellen') + ': ' + error);
});
}
/**
* 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('/dolibarr/custom/subtotaltitle/ajax/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 {
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 : {};
var newTitle = prompt(lang.sectionName || 'Positionsgruppe umbenennen:', currentTitle);
if (!newTitle) return;
debugLog('✏️ Benenne Section ' + sectionId + ' um zu: ' + newTitle);
$.post('/dolibarr/custom/subtotaltitle/ajax/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 {
alert((lang.errorSavingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSavingSection || 'Fehler beim Umbenennen') + ': ' + error);
});
}
/**
* Löscht eine leere Section
*/
function deleteSection(sectionId) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmDeleteSection || 'Leere Positionsgruppe löschen?')) {
return;
}
debugLog('🗑️ Lösche leere Section ' + sectionId);
$.post('/dolibarr/custom/subtotaltitle/ajax/delete_section.php', {
section_id: sectionId,
force: 0 // Nur leere löschen
}, function(response) {
debugLog('Delete response: ' + JSON.stringify(response));
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
} else {
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorDeletingSection || 'Fehler beim Löschen') + ': ' + error);
});
}
/**
* 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 || '⚠️ ACHTUNG!\n\nWollen Sie wirklich die Positionsgruppe\nUND alle %s enthaltenen Produkte löschen?\n\nDiese Aktion kann nicht rückgängig gemacht werden!').replace('%s', productCount);
if (!confirm(msg1)) {
return;
}
var msg2 = (lang.confirmDeleteSectionForce2 || 'Sind Sie WIRKLICH sicher?\n\n%s Produkte werden unwiderruflich gelöscht!').replace('%s', productCount);
if (!confirm(msg2)) {
return;
}
debugLog('🔴 Force-Delete Section ' + sectionId + ' mit Produkten: ' + JSON.stringify(productIds));
$.post('/dolibarr/custom/subtotaltitle/ajax/delete_section.php', {
section_id: sectionId,
force: 1,
product_ids: JSON.stringify(productIds)
}, function(response) {
debugLog('Force-Delete response: ' + JSON.stringify(response));
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
} else {
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorDeletingSection || 'Fehler beim Löschen') + ': ' + error);
});
}
/**
* 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('/dolibarr/custom/subtotaltitle/ajax/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 = '<tr class="section-header section-empty" data-section-id="' + section.id + '" data-section-title="' + section.title + '" data-line-order="' + section.line_order + '" data-product-count="0" data-product-ids="[]">';
// Titel (colspan=10)
sectionHtml += '<td colspan="9" style="font-weight:bold; padding:8px;">';
sectionHtml += '<span class="section-toggle" onclick="toggleSection(' + section.id + '); event.stopPropagation();">▼</span>';
sectionHtml += section.title;
sectionHtml += '<span class="section-count">0 Produkte</span>';
// Buttons
sectionHtml += '</td><td align="right" style="white-space:nowrap;">';
sectionHtml += '<a href="#" onclick="moveSection(' + section.id + ', \'up\'); return false;" title="Nach oben"><span class="fa fa-long-arrow-alt-up"></span></a>';
sectionHtml += '<a href="#" onclick="moveSection(' + section.id + ', \'down\'); return false;" title="Nach unten"><span class="fa fa-long-arrow-alt-down"></span></a>';
sectionHtml += '</td>';
// Edit (Spalte 11)
sectionHtml += '<td class="linecoledit center">';
sectionHtml += '<a href="#" onclick="renameSection(' + section.id + '); return false;" title="Umbenennen">';
sectionHtml += '<span class="fas fa-pencil-alt" style="color:#444;" title="Ändern"></span></a>';
sectionHtml += '</td>';
// Delete (Spalte 12) - leere Section = normaler Mülleimer
sectionHtml += '<td class="linecoldelete center">';
sectionHtml += '<a href="#" onclick="deleteSection(' + section.id + '); return false;" title="Leere Gruppe löschen">';
sectionHtml += '<span class="fas fa-trash pictodelete" title="Löschen"></span></a>';
sectionHtml += '</td>';
// Move (Spalte 13)
sectionHtml += '<td class="linecolmove tdlineupdown center"></td>';
// Unlink (Spalte 14)
sectionHtml += '<td class="linecolunlink"></td>';
sectionHtml += '</tr>';
// 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('/dolibarr/custom/subtotaltitle/ajax/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('/dolibarr/custom/subtotaltitle/ajax/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('<th class="linecolunlink" style="width:24px;"></th>');
}
});
// 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');
if (parentSection && parentSection !== 'null') {
// Hat Section → Unlink-Button
$row.append('<td class="linecolunlink center"><a href="#" onclick="removeFromSection(' + lineId + '); return false;" title="Aus Positionsgruppe entfernen"><span class="fas fa-unlink" style="color:#888;"></span></a></td>');
} else {
// Keine Section → leere Zelle
$row.append('<td class="linecolunlink"></td>');
}
return;
}
// Alle anderen Zeilen (trlinefordates, liste_titre, etc.): leere Zelle
if ($row.find('td').length > 0) {
$row.append('<td class="linecolunlink"></td>');
}
});
debugLog('✅ Unlink-Spalte hinzugefügt');
}
/**
* PLAN B: Section-Assignment beim Produkt hinzufügen
*/
$(document).ready(function() {
// Unlink-Spalte hinzufügen (nach data-Attributen)
setTimeout(addUnlinkColumn, 600);
// 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
*/
function assignLastProductToSection(sectionId, factureId) {
var docInfo = getDocumentInfo();
debugLog('🎯 Weise neustes Produkt zu Section ' + sectionId + ' zu (docType: ' + docInfo.type + ')...');
$.post('/dolibarr/custom/subtotaltitle/ajax/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');
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: '/dolibarr/custom/subtotaltitle/ajax/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 = '<tr class="subtotal-row' + inClass + '" data-section-id="' + sectionId + '"';
if (subtotalId) {
html += ' data-subtotal-id="' + subtotalId + '"';
}
html += ' style="font-weight:bold; background:#f5f5f5;">';
html += '<td colspan="9" style="text-align:right; padding:8px;">';
html += 'Zwischensumme:';
// Checkbox nur wenn Subtotal in DB existiert
if (subtotalId) {
html += ' <label style="font-weight:normal;font-size:12px;margin-left:10px;" title="In Rechnung/PDF anzeigen">';
html += '<input type="checkbox" class="sync-checkbox" data-line-id="' + subtotalId + '" data-line-type="subtotal" ' + syncChecked;
html += ' onclick="toggleFacturedetSync(' + subtotalId + ', \'subtotal\', this);">';
html += ' 📄</label>';
}
html += '</td>';
html += '<td class="linecolht right" style="padding:8px;">' + formattedSum + ' €</td>';
html += '<td class="linecoledit"></td>';
html += '<td class="linecoldelete"></td>';
html += '<td class="linecolmove"></td>';
html += '<td class="linecolunlink"></td>';
html += '</tr>';
$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('/dolibarr/custom/subtotaltitle/ajax/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: '/dolibarr/custom/subtotaltitle/ajax/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') {
// 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);
}
});
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 = '<tr class="textline-row drag" data-textline-id="' + textline.id + '" data-line-order="' + textline.line_order + '">';
// Inhalt (colspan=10)
html += '<td colspan="10" style="padding:8px; font-style:italic; font-weight:bold;">';
html += textline.title;
html += '</td>';
// Edit (Spalte 11)
html += '<td class="linecoledit center">';
html += '<a href="#" onclick="editTextLine(' + textline.id + '); return false;" title="Bearbeiten">';
html += '<span class="fas fa-pencil-alt" style="color:#444;"></span></a>';
html += '</td>';
// Delete (Spalte 12)
html += '<td class="linecoldelete center">';
html += '<a href="#" onclick="deleteTextLine(' + textline.id + '); return false;" title="Löschen">';
html += '<span class="fas fa-trash pictodelete"></span></a>';
html += '</td>';
// Move (Spalte 13)
html += '<td class="linecolmove tdlineupdown center"></td>';
// Unlink (Spalte 14)
html += '<td class="linecolunlink"></td>';
html += '</tr>';
// 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 text = prompt(lang.textlineContent || 'Text eingeben:');
if (!text) return;
var docInfo = getDocumentInfo();
if (!docInfo.id) {
alert(lang.errorLoadingSections || 'Fehler: Keine Dokument-ID gefunden');
return;
}
debugLog('Erstelle Textzeile für ' + docInfo.type + ' ID ' + docInfo.id);
$.post('/dolibarr/custom/subtotaltitle/ajax/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 {
alert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSavingTextline || 'Fehler beim Erstellen') + ': ' + error);
});
}
/**
* Textzeile bearbeiten
*/
function editTextLine(textlineId, currentText) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
var newText = prompt(lang.textlineContent || 'Text bearbeiten:', currentText || '');
if (!newText) return;
debugLog('✏️ Bearbeite Textzeile ' + textlineId);
$.post('/dolibarr/custom/subtotaltitle/ajax/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 {
alert((lang.errorSavingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorSavingTextline || 'Fehler beim Bearbeiten') + ': ' + error);
});
}
/**
* Textzeile löschen
*/
function deleteTextLine(textlineId) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmDeleteTextline || 'Textzeile wirklich löschen?')) {
return;
}
debugLog('🗑️ Lösche Textzeile ' + textlineId);
$.post('/dolibarr/custom/subtotaltitle/ajax/delete_textline.php', {
textline_id: textlineId
}, function(response) {
debugLog('Delete response: ' + JSON.stringify(response));
if (response.success) {
window.location.href = window.location.pathname + window.location.search;
} else {
alert((lang.errorDeletingTextline || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorDeletingTextline || 'Fehler beim Löschen') + ': ' + error);
});
}
/**
* Entfernt ein Produkt aus seiner Section
*/
function removeFromSection(productId) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
if (!confirm(lang.confirmRemoveFromSection || 'Produkt aus Positionsgruppe entfernen?')) {
return;
}
var docType = getDocumentType();
debugLog('🔓 Entferne Produkt ' + productId + ' aus Section (docType: ' + docType + ')');
$.post('/dolibarr/custom/subtotaltitle/ajax/remove_from_section.php', {
product_id: productId,
document_type: docType
}, function(response) {
debugLog('Remove response: ' + JSON.stringify(response));
if (response.success) {
safeReload();
} else {
alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
}
}, 'json').fail(function(xhr, status, error) {
debugLog('AJAX Fehler: ' + status + ' ' + error);
alert((lang.errorReordering || 'Fehler') + ': ' + error);
});
}
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(
'<input type="checkbox" class="mass-delete-checkbox" data-line-id="' + lineId + '" style="margin-right:8px;">'
);
}
});
// Buttons NACH dem Massenlösch-Button einfügen
$('#btnMassDelete').after(
'<a id="btnMassDoDelete" class="butActionDelete" href="#" onclick="deleteMassSelected(); return false;" style="background:#c00;color:#fff;margin-left:5px;">Ausgewählte löschen</a>' +
'<a id="btnMassSelectAll" class="butAction" href="#" onclick="selectAllLines(); return false;">Alle auswählen</a>' +
'<a id="btnMassCancel" class="butAction" href="#" onclick="toggleMassDelete(); return false;">Abbrechen</a>'
);
// 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) {
alert(lang.noLinesSelected || 'Keine Zeilen ausgewählt!');
return;
}
var msg1 = (lang.confirmDeleteLines || 'Wirklich %s Zeilen löschen?').replace('%s', selectedIds.length);
if (!confirm(msg1)) {
return;
}
var msg2 = (lang.confirmDeleteLinesWarning || 'LETZTE WARNUNG: %s Zeilen werden UNWIDERRUFLICH gelöscht!').replace('%s', selectedIds.length);
if (!confirm(msg2)) {
return;
}
$.post('/dolibarr/custom/subtotaltitle/ajax/mass_delete.php', {
line_ids: JSON.stringify(selectedIds),
facture_id: getFactureId()
}, function(response) {
if (response.success) {
safeReload();
} else {
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannt'));
}
}, 'json');
}
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 + ')';
}