subtotaltitle/js/subtotaltitle.js

1811 lines
66 KiB
JavaScript
Executable file

// 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 = $('<div/>', {
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 = $('<div/>', {
id: dialogId,
'class': 'subtotal-error-dialog',
title: 'Fehler'
}).appendTo('body');
$dialog.html('<div class="error" style="padding:10px;">' + message + '</div>');
$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 = $('<div/>', {
id: dialogId,
'class': 'subtotal-input-dialog',
title: title
}).appendTo('body');
var content = '<div style="padding:10px 0;">';
content += '<label for="' + inputId + '" style="display:block;margin-bottom:8px;">' + label + '</label>';
content += '<input type="text" id="' + inputId + '" class="flat minwidth300" style="width:100%;padding:8px;" value="' + defaultValue.replace(/"/g, '&quot;') + '">';
content += '</div>';
$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 = '<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;
// 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 || 'Produktgruppe erstellen',
lang.sectionName || 'Name der Produktgruppe:',
'',
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.sectionEdit || 'Produktgruppe umbenennen',
lang.sectionName || 'Name der Produktgruppe:',
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',
'<div style="color:#c00;"><strong>WARNUNG!</strong></div><br>' + msg1 + '<br><br><em>Diese Aktion kann nicht rückgängig gemacht werden!</em>',
function() {
var msg2 = (lang.confirmDeleteSectionForce2 || 'Sind Sie WIRKLICH sicher? %s Produkte werden unwiderruflich gelöscht!').replace('%s', productCount);
showConfirmDialog(
'Letzte Warnung',
'<div style="color:#c00;font-weight:bold;">' + msg2 + '</div>',
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 = '<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(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;
// NUR auf Dokumentdetailseiten ausführen (card), NICHT auf Listen
// Prüfe ob wir auf einer gültigen Seite sind (invoicecard, propalcard, ordercard)
var url = window.location.href;
var isDocumentCard = (url.indexOf('/facture/card.php') !== -1 ||
url.indexOf('/propal/card.php') !== -1 ||
url.indexOf('/commande/card.php') !== -1);
// Zusätzliche Prüfung: Modul muss aktiv sein (subtotalTitleIsDraft wird von PHP gesetzt)
var isModuleActive = (typeof subtotalTitleIsDraft !== 'undefined');
if (!isDocumentCard) {
debugLog('🔗 Keine Dokumentdetailseite, Unlink-Spalte wird übersprungen');
return;
}
if (!isModuleActive) {
debugLog('🔗 SubtotalTitle Modul nicht aktiv auf dieser Seite, Unlink-Spalte wird übersprungen');
return;
}
// Prüfe ob mindestens eine Produktgruppe (Section) vorhanden ist
var hasSections = ($('tr.section-header').length > 0);
if (!hasSections) {
debugLog('🔗 Keine Produktgruppen vorhanden, Unlink-Spalte wird übersprungen');
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 (Sections vorhanden)...');
// 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');
// 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('<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 → Link-Button (zu passender Section hinzufügen)
$row.append('<td class="linecolunlink center"><a href="#" onclick="linkToNearestSection(' + lineId + '); return false;" title="Zu Positionsgruppe hinzufügen"><span class="fas fa-link" style="color:#888;"></span></a></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 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 = '<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(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 = '<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 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(
'<input type="checkbox" class="mass-delete-checkbox" data-line-id="' + lineId + '" style="margin-right:8px;">'
);
}
});
// Buttons NACH dem Massenlösch-Button einfügen (Ausgewählte löschen ganz rechts)
$('#btnMassDelete').after(
'<a id="btnMassCancel" class="butAction" href="#" onclick="toggleMassDelete(); return false;">Abbrechen</a>' +
'<a id="btnMassSelectAll" class="butAction" href="#" onclick="selectAllLines(); return false;">Alle auswählen</a>' +
'<a id="btnMassDoDelete" class="butActionDelete" href="#" onclick="deleteMassSelected(); return false;" style="background:#c00;color:#fff;">Ausgewählte löschen</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) {
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', '<div style="color:#c00;font-weight:bold;">' + msg2 + '</div>', 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'
);
}