Fehlerbehebungen Drag and Drop, Checkboxen, Anzeige Zwischensumme. Fehler in New Order fix_sections

This commit is contained in:
Eduard Wisch 2026-01-27 13:22:39 +01:00
parent 050f2316b2
commit 954329d701
12 changed files with 718 additions and 183 deletions

View file

@ -46,17 +46,40 @@ $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_line']." = ".(int)$product_id; $sql .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
$resql = $db->query($sql); $resql = $db->query($sql);
if ($db->num_rows($resql) == 0) { // Hole die line_order der Section (Produkt soll direkt danach kommen)
// Produkt fehlt - hinzufügen $sql_section = "SELECT line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$section_id;
$next_order = 1; $resql_section = $db->query($sql_section);
$sql_max = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $section_order = 1;
$sql_max .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id; if ($obj_section = $db->fetch_object($resql_section)) {
$sql_max .= " AND document_type = '".$db->escape($docType)."'"; $section_order = $obj_section->line_order;
$resql_max = $db->query($sql_max);
if ($obj = $db->fetch_object($resql_max)) {
$next_order = ($obj->max_order ? $obj->max_order + 1 : 1);
} }
// Berechne neue line_order: Höchste line_order der Produkte in dieser Section + 1
// Oder Section line_order + 1 wenn keine Produkte vorhanden
$sql_max_in_section = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_max_in_section .= " WHERE parent_section = ".(int)$section_id;
$sql_max_in_section .= " AND line_type = 'product'";
$resql_max_section = $db->query($sql_max_in_section);
$obj_max_section = $db->fetch_object($resql_max_section);
if ($obj_max_section && $obj_max_section->max_order) {
$new_line_order = $obj_max_section->max_order + 1;
} else {
$new_line_order = $section_order + 1;
}
subtotaltitle_debug_log(' → Section line_order='.$section_order.', neue Produkt line_order='.$new_line_order);
// Verschiebe alle nachfolgenden Zeilen um 1 nach hinten
$sql_shift = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_shift .= " SET line_order = line_order + 1";
$sql_shift .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_shift .= " AND document_type = '".$db->escape($docType)."'";
$sql_shift .= " AND line_order >= ".$new_line_order;
$db->query($sql_shift);
if ($db->num_rows($resql) == 0) {
// Produkt fehlt - hinzufügen
// Setze alle FK-Felder explizit (NULL für nicht genutzte) // Setze alle FK-Felder explizit (NULL für nicht genutzte)
$fk_facture = ($docType === 'invoice') ? (int)$facture_id : 'NULL'; $fk_facture = ($docType === 'invoice') ? (int)$facture_id : 'NULL';
$fk_propal = ($docType === 'propal') ? (int)$facture_id : 'NULL'; $fk_propal = ($docType === 'propal') ? (int)$facture_id : 'NULL';
@ -64,20 +87,21 @@ if ($db->num_rows($resql) == 0) {
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_ins .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, ".$tables['fk_line'].", parent_section, line_order, date_creation)"; $sql_ins .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, ".$tables['fk_line'].", parent_section, line_order, date_creation)";
$sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'product', ".(int)$product_id.", ".(int)$section_id.", ".$next_order.", NOW())"; $sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'product', ".(int)$product_id.", ".(int)$section_id.", ".$new_line_order.", NOW())";
$db->query($sql_ins); $db->query($sql_ins);
subtotaltitle_debug_log(' → Produkt zu Manager-Tabelle hinzugefügt (line_order=' . $next_order . ')'); subtotaltitle_debug_log(' → Produkt zu Manager-Tabelle hinzugefügt (line_order=' . $new_line_order . ')');
} else { } else {
// Produkt existiert - UPDATE parent_section // Produkt existiert - UPDATE parent_section UND line_order
subtotaltitle_debug_log('🔵🔵🔵 assign_last_product: Produkt #'.$product_id.' → parent_section='.$section_id); subtotaltitle_debug_log('🔵🔵🔵 assign_last_product: Produkt #'.$product_id.' → parent_section='.$section_id.', line_order='.$new_line_order);
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_upd .= " SET parent_section = ".(int)$section_id; $sql_upd .= " SET parent_section = ".(int)$section_id;
$sql_upd .= ", line_order = ".$new_line_order;
$sql_upd .= " WHERE ".$tables['fk_line']." = ".(int)$product_id; $sql_upd .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
$db->query($sql_upd); $db->query($sql_upd);
subtotaltitle_debug_log(' → parent_section updated'); subtotaltitle_debug_log(' → parent_section und line_order updated');
} }
// Neu sortieren // Neu sortieren

48
ajax/check_subtotal.php Normal file
View file

@ -0,0 +1,48 @@
<?php
/**
* Prüft ob ein Subtotal für eine Section existiert
*/
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$section_id = GETPOST('section_id', 'int');
$docType = GETPOST('document_type', 'alpha');
if (!$section_id || !$docType) {
echo json_encode(['exists' => false, 'error' => 'Missing parameters']);
exit;
}
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['exists' => false, 'error' => 'Invalid document type']);
exit;
}
// Prüfe ob Subtotal in Manager-Tabelle existiert
$sql = "SELECT rowid, ".$tables['fk_line']." as detail_id, in_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE parent_section = ".(int)$section_id;
$sql .= " AND line_type = 'subtotal'";
$sql .= " AND document_type = '".$db->escape($docType)."'";
$resql = $db->query($sql);
$exists = false;
$subtotal_id = null;
$detail_id = null;
$in_facturedet = false;
if ($resql && $db->num_rows($resql) > 0) {
$obj = $db->fetch_object($resql);
$exists = true;
$subtotal_id = $obj->rowid;
$detail_id = $obj->detail_id;
$in_facturedet = $obj->in_facturedet ? true : false;
}
echo json_encode([
'exists' => $exists,
'subtotal_id' => $subtotal_id,
'detail_id' => $detail_id,
'in_facturedet' => $in_facturedet
]);

144
ajax/cleanup_subtotals.php Normal file
View file

@ -0,0 +1,144 @@
<?php
/**
* Bereinigt verwaiste Subtotals und fehlerhafte Einträge
*/
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$facture_id = GETPOST('facture_id', 'int');
$docType = GETPOST('document_type', 'alpha');
if (!$facture_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit;
}
$db->begin();
$deleted = 0;
$fixed = 0;
// 0. Sections dürfen KEINE parent_section haben - korrigiere das zuerst
$sql_fix_sections = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_fix_sections .= " SET parent_section = NULL";
$sql_fix_sections .= " WHERE line_type = 'section'";
$sql_fix_sections .= " AND parent_section IS NOT NULL";
$sql_fix_sections .= " AND ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_fix_sections .= " AND document_type = '".$db->escape($docType)."'";
$db->query($sql_fix_sections);
$sections_fixed = $db->affected_rows();
if ($sections_fixed > 0) {
subtotaltitle_debug_log('🧹 ' . $sections_fixed . ' Sections mit falscher parent_section korrigiert');
$fixed += $sections_fixed;
}
// 0b. parent_section = 0 sollte NULL sein
$sql_fix_zero = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_fix_zero .= " SET parent_section = NULL";
$sql_fix_zero .= " WHERE parent_section = 0";
$sql_fix_zero .= " AND ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_fix_zero .= " AND document_type = '".$db->escape($docType)."'";
$db->query($sql_fix_zero);
$zero_fixed = $db->affected_rows();
if ($zero_fixed > 0) {
subtotaltitle_debug_log('🧹 ' . $zero_fixed . ' Einträge mit parent_section=0 korrigiert');
$fixed += $zero_fixed;
}
// 1. Lösche fehlerhafte "Produkte" in der Detail-Tabelle die eigentlich Zwischensummen sind
// (erkennbar an description LIKE 'Zwischensumme%' aber OHNE special_code 102)
$sql_bad = "SELECT rowid FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_bad .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_bad .= " AND description LIKE 'Zwischensumme%'";
$sql_bad .= " AND (special_code IS NULL OR special_code != 102)";
$resql_bad = $db->query($sql_bad);
while ($obj = $db->fetch_object($resql_bad)) {
// Lösche aus Detail-Tabelle
$sql_del = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_del);
// Lösche auch aus Manager-Tabelle falls vorhanden
$sql_del_mgr = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_del_mgr .= " WHERE ".$tables['fk_line']." = ".(int)$obj->rowid;
$db->query($sql_del_mgr);
subtotaltitle_debug_log('🧹 Fehlerhaftes Zwischensummen-Produkt gelöscht: #' . $obj->rowid);
$fixed++;
}
// 2. Lösche Subtotals deren Section show_subtotal = 0 hat
$sql = "SELECT sub.rowid, sub.".$tables['fk_line']." as detail_id";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager sub";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."facture_lines_manager sec ON sec.rowid = sub.parent_section";
$sql .= " WHERE sub.line_type = 'subtotal'";
$sql .= " AND sub.".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " AND sub.document_type = '".$db->escape($docType)."'";
$sql .= " AND (sec.show_subtotal = 0 OR sec.show_subtotal IS NULL)";
$resql = $db->query($sql);
while ($obj = $db->fetch_object($resql)) {
// Auch aus Detail-Tabelle löschen falls vorhanden
if ($obj->detail_id) {
$sql_del_det = "DELETE FROM ".MAIN_DB_PREFIX.$tables['lines_table']." WHERE rowid = ".(int)$obj->detail_id;
$db->query($sql_del_det);
}
// Aus Manager löschen
$sql_del = "DELETE FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_del);
$deleted++;
}
if ($deleted > 0 || $fixed > 0) {
subtotaltitle_debug_log('🧹 Cleanup: ' . $deleted . ' verwaiste Subtotals, ' . $fixed . ' fehlerhafte Produkte gelöscht');
// line_order neu durchnummerieren
$sql_reorder = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_reorder .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_reorder .= " AND document_type = '".$db->escape($docType)."'";
$sql_reorder .= " ORDER BY line_order";
$resql = $db->query($sql_reorder);
$new_order = 1;
while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_upd .= " SET line_order = ".$new_order;
$sql_upd .= " WHERE rowid = ".(int)$obj->rowid;
$db->query($sql_upd);
$new_order++;
}
// Auch rang in Detail-Tabelle neu durchnummerieren
$sql_sync = "SELECT ".$tables['fk_line']." FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_sync .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id;
$sql_sync .= " AND document_type = '".$db->escape($docType)."'";
$sql_sync .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql_sync .= " ORDER BY line_order";
$resql_sync = $db->query($sql_sync);
$rang = 1;
while ($obj = $db->fetch_object($resql_sync)) {
$fk_line_value = $obj->{$tables['fk_line']};
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$fk_line_value;
$db->query($sql_upd);
$rang++;
}
}
$db->commit();
echo json_encode([
'success' => true,
'deleted' => $deleted,
'fixed' => $fixed
]);

View file

@ -40,12 +40,8 @@ $sql .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->esc
if ($db->query($sql)) { if ($db->query($sql)) {
$section_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager"); $section_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager");
// Erstelle automatisch auch eine Zwischensumme für diese Section // KEIN automatisches Subtotal mehr - wird nur erstellt wenn Checkbox aktiviert wird
$subtotal_order = $next_order + 1000; // Hohe Nummer, wird später normalisiert // Das Subtotal wird über toggle_subtotal.php erstellt/gelöscht
$sql_subtotal = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_subtotal .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, title, parent_section, line_order, date_creation)";
$sql_subtotal .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'subtotal', 'Zwischensumme', ".(int)$section_id.", ".$subtotal_order.", NOW())";
$db->query($sql_subtotal);
echo json_encode(['success' => true, 'section_id' => $section_id]); echo json_encode(['success' => true, 'section_id' => $section_id]);
} else { } else {

58
ajax/fix_sections.php Normal file
View file

@ -0,0 +1,58 @@
<?php
/**
* Repariert fehlerhafte parent_section Werte
* - Sections sollten KEINE parent_section haben
* - parent_section = 0 sollte NULL sein
*/
define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php';
$doc_id = GETPOST('doc_id', 'int');
$docType = GETPOST('document_type', 'alpha');
if (!$doc_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
$db->begin();
$fixed = 0;
// 1. Sections dürfen KEINE parent_section haben
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET parent_section = NULL";
$sql .= " WHERE line_type = 'section'";
$sql .= " AND parent_section IS NOT NULL";
$sql .= " AND document_type = '".$db->escape($docType)."'";
if ($docType == 'invoice') {
$sql .= " AND fk_facture = ".(int)$doc_id;
} elseif ($docType == 'propal') {
$sql .= " AND fk_propal = ".(int)$doc_id;
} elseif ($docType == 'order') {
$sql .= " AND fk_commande = ".(int)$doc_id;
}
$db->query($sql);
$fixed += $db->affected_rows();
// 2. parent_section = 0 sollte NULL sein
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET parent_section = NULL";
$sql .= " WHERE parent_section = 0";
$sql .= " AND document_type = '".$db->escape($docType)."'";
if ($docType == 'invoice') {
$sql .= " AND fk_facture = ".(int)$doc_id;
} elseif ($docType == 'propal') {
$sql .= " AND fk_propal = ".(int)$doc_id;
} elseif ($docType == 'order') {
$sql .= " AND fk_commande = ".(int)$doc_id;
}
$db->query($sql);
$fixed += $db->affected_rows();
$db->commit();
echo json_encode([
'success' => true,
'fixed' => $fixed,
'message' => $fixed . ' Einträge korrigiert'
]);

View file

@ -22,7 +22,7 @@ if (!$tables) {
// Hole ALLE Sections für diesen Dokumenttyp // Hole ALLE Sections für diesen Dokumenttyp
$sql = "SELECT s.rowid, s.title, s.line_order, "; $sql = "SELECT s.rowid, s.title, s.line_order, ";
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."facture_lines_manager p WHERE p.parent_section = s.rowid) as product_count"; $sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."facture_lines_manager p WHERE p.parent_section = s.rowid AND p.line_type = 'product' AND p.document_type = '".$db->escape($docType)."') as product_count";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager s"; $sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager s";
$sql .= " WHERE s.".$tables['fk_parent']." = ".(int)$facture_id; $sql .= " WHERE s.".$tables['fk_parent']." = ".(int)$facture_id;
$sql .= " AND s.document_type = '".$db->escape($docType)."'"; $sql .= " AND s.document_type = '".$db->escape($docType)."'";

View file

@ -1,19 +1,41 @@
<?php <?php
define('NOTOKENRENEWAL', 1); define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php'; require '../../../main.inc.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$section_id = GETPOST('section_id', 'int'); $section_id = GETPOST('section_id', 'int');
$direction = GETPOST('direction', 'alpha'); $direction = GETPOST('direction', 'alpha');
$docType = GETPOST('document_type', 'alpha');
if (!$section_id || !$direction) { if (!$section_id || !$direction) {
echo json_encode(array('success' => false, 'error' => 'Missing parameters')); echo json_encode(array('success' => false, 'error' => 'Missing parameters'));
exit; exit;
} }
// Wenn kein docType übergeben, versuche ihn aus der Section zu ermitteln
if (!$docType) {
$sql = "SELECT document_type FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$section_id;
$res = $db->query($sql);
if ($res && $obj = $db->fetch_object($res)) {
$docType = $obj->document_type;
}
}
if (!$docType) {
$docType = 'invoice'; // Fallback
}
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(array('success' => false, 'error' => 'Invalid document type'));
exit;
}
// Hole Section-Info // Hole Section-Info
$sql = "SELECT fk_facture FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "SELECT ".$tables['fk_parent']." as doc_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE rowid = ".(int)$section_id; $sql .= " WHERE rowid = ".(int)$section_id;
$sql .= " AND line_type = 'section'"; $sql .= " AND line_type = 'section'";
$sql .= " AND document_type = '".$db->escape($docType)."'";
$resql = $db->query($sql); $resql = $db->query($sql);
if (!$resql || $db->num_rows($resql) == 0) { if (!$resql || $db->num_rows($resql) == 0) {
@ -22,13 +44,14 @@ if (!$resql || $db->num_rows($resql) == 0) {
} }
$section = $db->fetch_object($resql); $section = $db->fetch_object($resql);
$facture_id = $section->fk_facture; $doc_id = $section->doc_id;
$db->begin(); $db->begin();
// 1. Hole alle Sections (sortiert) // 1. Hole alle Sections (sortiert)
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'section'"; $sql .= " AND line_type = 'section'";
$sql .= " ORDER BY line_order"; $sql .= " ORDER BY line_order";
$resql = $db->query($sql); $resql = $db->query($sql);
@ -42,18 +65,21 @@ while ($obj = $db->fetch_object($resql)) {
$current_index = array_search($section_id, $sections); $current_index = array_search($section_id, $sections);
if ($current_index === false) { if ($current_index === false) {
$db->rollback();
echo json_encode(array('success' => false, 'error' => 'Section not in list')); echo json_encode(array('success' => false, 'error' => 'Section not in list'));
exit; exit;
} }
if ($direction == 'up') { if ($direction == 'up') {
if ($current_index == 0) { if ($current_index == 0) {
$db->rollback();
echo json_encode(array('success' => false, 'error' => 'Already at top')); echo json_encode(array('success' => false, 'error' => 'Already at top'));
exit; exit;
} }
$swap_index = $current_index - 1; $swap_index = $current_index - 1;
} else { } else {
if ($current_index == count($sections) - 1) { if ($current_index == count($sections) - 1) {
$db->rollback();
echo json_encode(array('success' => false, 'error' => 'Already at bottom')); echo json_encode(array('success' => false, 'error' => 'Already at bottom'));
exit; exit;
} }
@ -71,7 +97,8 @@ $updates = array();
// Freie Produkte zuerst // Freie Produkte zuerst
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'"; $sql .= " AND line_type = 'product'";
$sql .= " AND parent_section IS NULL"; $sql .= " AND parent_section IS NULL";
$sql .= " ORDER BY line_order"; $sql .= " ORDER BY line_order";
@ -84,7 +111,8 @@ while ($obj = $db->fetch_object($resql)) {
// Freie Textzeilen // Freie Textzeilen
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'text'"; $sql .= " AND line_type = 'text'";
$sql .= " AND parent_section IS NULL"; $sql .= " AND parent_section IS NULL";
$sql .= " ORDER BY line_order"; $sql .= " ORDER BY line_order";
@ -103,7 +131,8 @@ foreach ($sections as $sec_id) {
// Produkte dieser Section // Produkte dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'"; $sql .= " AND line_type = 'product'";
$sql .= " AND parent_section = ".(int)$sec_id; $sql .= " AND parent_section = ".(int)$sec_id;
$sql .= " ORDER BY line_order"; $sql .= " ORDER BY line_order";
@ -116,7 +145,8 @@ foreach ($sections as $sec_id) {
// Textzeilen dieser Section // Textzeilen dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'text'"; $sql .= " AND line_type = 'text'";
$sql .= " AND parent_section = ".(int)$sec_id; $sql .= " AND parent_section = ".(int)$sec_id;
$sql .= " ORDER BY line_order"; $sql .= " ORDER BY line_order";
@ -127,9 +157,10 @@ foreach ($sections as $sec_id) {
$new_order++; $new_order++;
} }
// ========== SUBTOTAL DIESER SECTION ========== // Subtotal dieser Section
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'subtotal'"; $sql .= " AND line_type = 'subtotal'";
$sql .= " AND parent_section = ".(int)$sec_id; $sql .= " AND parent_section = ".(int)$sec_id;
$resql = $db->query($sql); $resql = $db->query($sql);
@ -140,6 +171,16 @@ foreach ($sections as $sec_id) {
} }
} }
// Freie Produkte am Ende (nach allen Sections)
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND line_type = 'product'";
$sql .= " AND parent_section IS NULL";
$sql .= " AND rowid NOT IN (SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE ".$tables['fk_parent']." = ".(int)$doc_id." AND document_type = '".$db->escape($docType)."' AND line_type = 'product' AND parent_section IS NULL AND line_order < (SELECT MIN(line_order) FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE ".$tables['fk_parent']." = ".(int)$doc_id." AND document_type = '".$db->escape($docType)."' AND line_type = 'section'))";
$sql .= " ORDER BY line_order";
// Diese Abfrage ist zu komplex - die freien Produkte wurden bereits oben behandelt
// 4. Führe alle Updates aus // 4. Führe alle Updates aus
foreach ($updates as $rowid => $order) { foreach ($updates as $rowid => $order) {
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
@ -148,21 +189,25 @@ foreach ($updates as $rowid => $order) {
$db->query($sql); $db->query($sql);
} }
// 5. Sync rang // 5. Sync rang in Detail-Tabelle
$sql = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "SELECT ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " WHERE fk_facture = ".(int)$facture_id; $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id;
$sql .= " AND line_type = 'product'"; $sql .= " AND document_type = '".$db->escape($docType)."'";
$sql .= " AND ".$tables['fk_line']." IS NOT NULL";
$sql .= " ORDER BY line_order"; $sql .= " ORDER BY line_order";
$resql = $db->query($sql); $resql = $db->query($sql);
$rang = 1; $rang = 1;
while ($obj = $db->fetch_object($resql)) { while ($obj = $db->fetch_object($resql)) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet"; $detail_id = $obj->detail_id;
if ($detail_id) {
$sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_upd .= " SET rang = ".$rang; $sql_upd .= " SET rang = ".$rang;
$sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet; $sql_upd .= " WHERE rowid = ".(int)$detail_id;
$db->query($sql_upd); $db->query($sql_upd);
$rang++; $rang++;
} }
}
$db->commit(); $db->commit();

View file

@ -2,23 +2,34 @@
define('NOTOKENRENEWAL', 1); define('NOTOKENRENEWAL', 1);
require '../../../main.inc.php'; require '../../../main.inc.php';
require_once __DIR__.'/../lib/subtotaltitle.lib.php'; require_once __DIR__.'/../lib/subtotaltitle.lib.php';
require_once __DIR__.'/../class/DocumentTypeHelper.class.php';
$product_id = GETPOST('product_id', 'int'); $product_id = GETPOST('product_id', 'int');
$docType = GETPOST('document_type', 'alpha');
subtotaltitle_debug_log('🔓 remove_from_section: product=' . $product_id); subtotaltitle_debug_log('🔓 remove_from_section: product=' . $product_id . ', docType=' . $docType);
if (!$product_id) { if (!$product_id || !$docType) {
echo json_encode(['success' => false, 'error' => 'Missing product_id']); echo json_encode(['success' => false, 'error' => 'Missing parameters']);
exit;
}
// Hole die richtigen Tabellennamen für diesen Dokumenttyp
$tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) {
echo json_encode(['success' => false, 'error' => 'Invalid document type']);
exit; exit;
} }
$sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager"; $sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= " SET parent_section = NULL"; $sql .= " SET parent_section = NULL";
$sql .= " WHERE fk_facturedet = ".(int)$product_id; $sql .= " WHERE ".$tables['fk_line']." = ".(int)$product_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$result = $db->query($sql); $result = $db->query($sql);
if ($result) { if ($result) {
subtotaltitle_debug_log('✅ Produkt #' . $product_id . ' aus Section entfernt');
echo json_encode(['success' => true]); echo json_encode(['success' => true]);
} else { } else {
echo json_encode(['success' => false, 'error' => $db->lasterror()]); echo json_encode(['success' => false, 'error' => $db->lasterror()]);

View file

@ -62,30 +62,39 @@ foreach ($new_order as $item) {
// ========== SUBTOTALS NEU POSITIONIEREN ========== // ========== SUBTOTALS NEU POSITIONIEREN ==========
subtotaltitle_debug_log('🔢 Repositioniere Subtotals...'); subtotaltitle_debug_log('🔢 Repositioniere Subtotals...');
// Hole alle Subtotals für dieses Dokument
$sql = "SELECT rowid, parent_section FROM ".MAIN_DB_PREFIX."facture_lines_manager $sql = "SELECT rowid, parent_section FROM ".MAIN_DB_PREFIX."facture_lines_manager
WHERE ".$tables['fk_parent']." = ".(int)$facture_id." WHERE ".$tables['fk_parent']." = ".(int)$facture_id."
AND document_type = '".$db->escape($docType)."' AND document_type = '".$db->escape($docType)."'
AND line_type = 'subtotal'"; AND line_type = 'subtotal'";
$resql = $db->query($sql); $resql = $db->query($sql);
$subtotals_to_update = array();
while ($subtotal = $db->fetch_object($resql)) { while ($subtotal = $db->fetch_object($resql)) {
$subtotals_to_update[] = $subtotal;
}
foreach ($subtotals_to_update as $subtotal) {
// Finde höchste line_order der Produkte dieser Section // Finde höchste line_order der Produkte dieser Section
$sql_max = "SELECT MAX(line_order) as max_order $sql_max = "SELECT MAX(line_order) as max_order
FROM ".MAIN_DB_PREFIX."facture_lines_manager FROM ".MAIN_DB_PREFIX."facture_lines_manager
WHERE parent_section = ".(int)$subtotal->parent_section." WHERE parent_section = ".(int)$subtotal->parent_section."
AND line_type = 'product'"; AND line_type = 'product'
AND document_type = '".$db->escape($docType)."'";
$res_max = $db->query($sql_max); $res_max = $db->query($sql_max);
$obj_max = $db->fetch_object($res_max); $obj_max = $db->fetch_object($res_max);
if ($obj_max && $obj_max->max_order) { if ($obj_max && $obj_max->max_order) {
// Subtotal bekommt hohe Nummer (wird gleich normalisiert) // Subtotal kommt direkt nach dem letzten Produkt: max_order + 0.5
$temp_order = (int)$obj_max->max_order * 100 + 50; $temp_order = (float)$obj_max->max_order + 0.5;
$sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager $sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager
SET line_order = ".$temp_order." SET line_order = ".$temp_order."
WHERE rowid = ".(int)$subtotal->rowid; WHERE rowid = ".(int)$subtotal->rowid;
$db->query($sql_upd); $db->query($sql_upd);
subtotaltitle_debug_log(' Subtotal #'.$subtotal->rowid.' → temp_order='.$temp_order.' (nach Section '.$subtotal->parent_section.')'); subtotaltitle_debug_log(' Subtotal #'.$subtotal->rowid.' (Section '.$subtotal->parent_section.') → temp_order='.$temp_order);
} else {
subtotaltitle_debug_log(' ⚠️ Subtotal #'.$subtotal->rowid.' hat keine Produkte in Section '.$subtotal->parent_section);
} }
} }

View file

@ -518,10 +518,8 @@ class ActionsSubtotalTitle extends CommonHookActions
// Prüfe ob diese Zeile eine unserer speziellen Zeilen ist (Section, Text, Subtotal) // Prüfe ob diese Zeile eine unserer speziellen Zeilen ist (Section, Text, Subtotal)
// special_code: 100=Section, 101=Text, 102=Subtotal // special_code: 100=Section, 101=Text, 102=Subtotal
if (isset($line->special_code) && in_array($line->special_code, array(100, 101, 102))) { if (isset($line->special_code) && in_array($line->special_code, array(100, 101, 102))) {
// Diese Zeile ist eine unserer speziellen Zeilen - per JS ausblenden // Diese Zeile wird von uns selbst gerendert - Original sofort per CSS verstecken
echo '<script>$(document).ready(function() { '; echo '<style>#row-'.$line->id.', tr[data-line-id="'.$line->id.'"] { display: none !important; }</style>';
echo ' $("tr[id*=\''.$line->id.'\']").hide();';
echo '});</script>';
return 0; return 0;
} }
@ -598,8 +596,15 @@ class ActionsSubtotalTitle extends CommonHookActions
$current_parent_section = $obj_current->parent_section; $current_parent_section = $obj_current->parent_section;
} }
// Subtotal der VORHERIGEN Section rendern (wenn Section-Wechsel) // Subtotal der VORHERIGEN Section rendern (wenn Section-Wechsel UND show_subtotal aktiviert)
if ($last_parent_section[$doc_key] && $last_parent_section[$doc_key] != $current_parent_section) { if ($last_parent_section[$doc_key] && $last_parent_section[$doc_key] != $current_parent_section) {
// Prüfe erst ob die Section show_subtotal aktiviert hat
$sql_check_show = "SELECT show_subtotal FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_check_show .= " WHERE rowid = ".(int)$last_parent_section[$doc_key];
$resql_check = $db->query($sql_check_show);
$section_obj = $db->fetch_object($resql_check);
if ($section_obj && $section_obj->show_subtotal) {
$sql_subtotal = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_subtotal = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_subtotal .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id; $sql_subtotal .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_subtotal .= " AND document_type = '".$db->escape($docType)."'"; $sql_subtotal .= " AND document_type = '".$db->escape($docType)."'";
@ -619,6 +624,7 @@ class ActionsSubtotalTitle extends CommonHookActions
} }
} }
} }
}
// Hole ALLE Sections ZWISCHEN letztem rang und aktuellem rang // Hole ALLE Sections ZWISCHEN letztem rang und aktuellem rang
$sql = "SELECT DISTINCT s.rowid, s.title, s.show_subtotal, s.collapsed, s.line_order, s.in_facturedet,"; $sql = "SELECT DISTINCT s.rowid, s.title, s.show_subtotal, s.collapsed, s.line_order, s.in_facturedet,";
@ -682,6 +688,55 @@ class ActionsSubtotalTitle extends CommonHookActions
// Merke für nächsten Durchlauf // Merke für nächsten Durchlauf
$last_rang[$doc_key] = $current_rang; $last_rang[$doc_key] = $current_rang;
$last_parent_section[$doc_key] = $current_parent_section; $last_parent_section[$doc_key] = $current_parent_section;
// Prüfe ob dies die LETZTE Produktzeile ist - dann Subtotal per JavaScript NACH dieser Zeile einfügen
if ($current_parent_section) {
// Hole max rang für dieses Dokument (nur echte Produkte, keine special_code 100-102)
$sql_max = "SELECT MAX(rang) as max_rang FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql_max .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_max .= " AND (special_code IS NULL OR special_code = 0 OR special_code < 100 OR special_code > 102)";
$res_max = $db->query($sql_max);
$obj_max = $db->fetch_object($res_max);
if ($obj_max && $current_rang >= $obj_max->max_rang) {
// Dies ist die letzte Produktzeile - Subtotal per JS NACH dieser Zeile einfügen
$sql_check_show = "SELECT show_subtotal FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_check_show .= " WHERE rowid = ".(int)$current_parent_section;
$resql_check = $db->query($sql_check_show);
$section_obj = $db->fetch_object($resql_check);
if ($section_obj && $section_obj->show_subtotal) {
$sql_subtotal = "SELECT rowid, title, parent_section, line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_subtotal .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql_subtotal .= " AND document_type = '".$db->escape($docType)."'";
$sql_subtotal .= " AND parent_section = ".(int)$current_parent_section;
$sql_subtotal .= " AND line_type = 'subtotal'";
$resql_subtotal = $db->query($sql_subtotal);
if ($obj_sub = $db->fetch_object($resql_subtotal)) {
$subtotal_key = 'subtotal_'.$obj_sub->rowid;
if (!in_array($subtotal_key, self::$rendered_sections[$doc_key])) {
// Subtotal-HTML generieren
$subtotalHtml = $this->renderSubtotalLine($obj_sub);
// Per JavaScript NACH der aktuellen Zeile einfügen
$escapedHtml = addslashes(str_replace(array("\r", "\n"), '', $subtotalHtml));
echo '<script>$(document).ready(function() { ';
echo ' var $lastRow = $("tr[data-line-id=\''.$line->id.'\']");';
echo ' if ($lastRow.length === 0) $lastRow = $("#row-'.$line->id.'");';
echo ' if ($lastRow.length > 0 && $lastRow.next(".subtotal-row").length === 0) {';
echo ' $lastRow.after("'.$escapedHtml.'");';
echo ' }';
echo '});</script>';
self::$rendered_sections[$doc_key][] = $subtotal_key;
if ($this->debug) {
error_log('[SubtotalTitle] ✅ Subtotal für letzte Section per JS eingefügt');
}
}
}
}
}
}
} }
/** /**
@ -839,18 +894,27 @@ class ActionsSubtotalTitle extends CommonHookActions
$sql_cleanup .= " AND d.rowid IS NULL"; $sql_cleanup .= " AND d.rowid IS NULL";
$result = $db->query($sql_cleanup); $result = $db->query($sql_cleanup);
// 2. Hole alle Produktzeilen des Dokuments // 2. Hole alle Produktzeilen des Dokuments mit rang
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX.$tables['lines_table']; $sql = "SELECT rowid, rang FROM ".MAIN_DB_PREFIX.$tables['lines_table'];
$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id; $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " ORDER BY rang"; $sql .= " ORDER BY rang";
$resql = $db->query($sql); $resql = $db->query($sql);
$new_products = array();
while ($obj = $db->fetch_object($resql)) { while ($obj = $db->fetch_object($resql)) {
$sql_check = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_check = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_check .= " WHERE ".$tables['fk_line']." = ".(int)$obj->rowid; $sql_check .= " WHERE ".$tables['fk_line']." = ".(int)$obj->rowid;
$resql_check = $db->query($sql_check); $resql_check = $db->query($sql_check);
if ($db->num_rows($resql_check) == 0) { if ($db->num_rows($resql_check) == 0) {
// Neues Produkt gefunden - merken mit rang
$new_products[] = array('rowid' => $obj->rowid, 'rang' => $obj->rang);
}
}
// 3. Füge neue Produkte ein - am Ende der Liste
if (count($new_products) > 0) {
// Hole einmal die höchste line_order
$next_order = $this->getNextLineOrder($document_id, $docType); $next_order = $this->getNextLineOrder($document_id, $docType);
// Setze alle FK-Felder explizit (NULL für nicht genutzte) // Setze alle FK-Felder explizit (NULL für nicht genutzte)
@ -858,10 +922,12 @@ class ActionsSubtotalTitle extends CommonHookActions
$fk_propal = ($docType === 'propal') ? (int)$document_id : 'NULL'; $fk_propal = ($docType === 'propal') ? (int)$document_id : 'NULL';
$fk_commande = ($docType === 'order') ? (int)$document_id : 'NULL'; $fk_commande = ($docType === 'order') ? (int)$document_id : 'NULL';
foreach ($new_products as $product) {
$sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_ins = "INSERT INTO ".MAIN_DB_PREFIX."facture_lines_manager";
$sql_ins .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, ".$tables['fk_line'].", line_order, date_creation)"; $sql_ins .= " (fk_facture, fk_propal, fk_commande, document_type, line_type, ".$tables['fk_line'].", line_order, date_creation)";
$sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'product', ".(int)$obj->rowid.", ".$next_order.", NOW())"; $sql_ins .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->escape($docType)."', 'product', ".(int)$product['rowid'].", ".$next_order.", NOW())";
$db->query($sql_ins); $db->query($sql_ins);
$next_order++; // Für jedes weitere Produkt erhöhen
} }
} }
} }
@ -1202,9 +1268,12 @@ class ActionsSubtotalTitle extends CommonHookActions
global $db; global $db;
$tables = DocumentTypeHelper::getTableNames($docType); $tables = DocumentTypeHelper::getTableNames($docType);
if (!$tables) return 1;
$sql = "SELECT MAX(line_order) as max_order"; $sql = "SELECT MAX(line_order) as max_order";
$sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " FROM ".MAIN_DB_PREFIX."facture_lines_manager";
$sql .= $this->getDocumentWhere($document_id, $docType); $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id;
$sql .= " AND document_type = '".$db->escape($docType)."'";
$resql = $db->query($sql); $resql = $db->query($sql);
if (!$resql || !($obj = $db->fetch_object($resql))) { if (!$resql || !($obj = $db->fetch_object($resql))) {
return 1; return 1;

View file

@ -1,5 +1,5 @@
// DEBUG FLAG - true für Debug-Ausgaben, false für Produktiv // DEBUG FLAG - true für Debug-Ausgaben, false für Produktiv
var SUBTOTAL_DEBUG = true; var SUBTOTAL_DEBUG = false;
function debugLog(message) { function debugLog(message) {
if (SUBTOTAL_DEBUG) { if (SUBTOTAL_DEBUG) {
@ -7,6 +7,36 @@ function debugLog(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? // Flag: Wird gerade gezogen?
var isDragging = false; var isDragging = false;
var isTogglingSubtotal = false; var isTogglingSubtotal = false;
@ -20,7 +50,8 @@ if (typeof SubtotalTitleLoaded === 'undefined') {
// Füge Button zu den Standard-Buttons hinzu - NUR im Entwurfsstatus // Füge Button zu den Standard-Buttons hinzu - NUR im Entwurfsstatus
if ($('#tablelines').length > 0) { if ($('#tablelines').length > 0) {
var factureId = getFactureId(); // Cleanup verwaister Subtotals (wo show_subtotal=0)
cleanupOrphanedSubtotals();
// Prüfe ob Dokument im Entwurfsstatus ist // Prüfe ob Dokument im Entwurfsstatus ist
if (typeof subtotalTitleIsDraft !== 'undefined' && subtotalTitleIsDraft === true) { if (typeof subtotalTitleIsDraft !== 'undefined' && subtotalTitleIsDraft === true) {
@ -34,7 +65,6 @@ if (typeof SubtotalTitleLoaded === 'undefined') {
debugLog('⚠️ Dokument nicht im Entwurfsstatus - Button wird nicht angezeigt'); debugLog('⚠️ Dokument nicht im Entwurfsstatus - Button wird nicht angezeigt');
} }
// ⬇️ HIER FEHLTE DER AUFRUF! ⬇️
initDragAndDrop(); initDragAndDrop();
} }
}); });
@ -104,11 +134,13 @@ function createNewSection() {
*/ */
function moveSection(sectionId, direction) { function moveSection(sectionId, direction) {
var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {}; var lang = (typeof subtotalTitleLang !== 'undefined') ? subtotalTitleLang : {};
debugLog('🔄 Verschiebe Section ' + sectionId + ' ' + direction); var docInfo = getDocumentInfo();
debugLog('🔄 Verschiebe Section ' + sectionId + ' ' + direction + ' (docType: ' + docInfo.type + ')');
$.post('/dolibarr/custom/subtotaltitle/ajax/move_section.php', { $.post('/dolibarr/custom/subtotaltitle/ajax/move_section.php', {
section_id: sectionId, section_id: sectionId,
direction: direction direction: direction,
document_type: docInfo.type
}, function(response) { }, function(response) {
debugLog('Move response: ' + JSON.stringify(response)); debugLog('Move response: ' + JSON.stringify(response));
if (response.success) { if (response.success) {
@ -401,25 +433,56 @@ function saveCurrentOrder() {
else if ($row.attr('id') && $row.attr('id').indexOf('row-') === 0) { else if ($row.attr('id') && $row.attr('id').indexOf('row-') === 0) {
var productId = $row.attr('id').replace('row-', ''); 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({ updates.push({
type: 'product', type: 'product',
id: productId, id: productId,
order: order, order: order,
parent_section: currentSectionId parent_section: assignToSection
}); });
debugLog(' ' + order + '. 📦 Produkt #' + productId + ' → ' + (currentSectionId ? 'Section ' + currentSectionId : 'FREI')); debugLog(' ' + order + '. 📦 Produkt #' + productId + ' → ' + (assignToSection ? 'Section ' + assignToSection : 'FREI'));
order++; order++;
} }
else if ($row.hasClass('textline-row')) { else if ($row.hasClass('textline-row')) {
var textlineId = $row.attr('data-textline-id'); var textlineId = $row.attr('data-textline-id');
if (textlineId) { 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({ updates.push({
type: 'text', type: 'text',
id: textlineId, id: textlineId,
order: order, order: order,
parent_section: currentSectionId parent_section: textAssignToSection
}); });
debugLog(' ' + order + '. 📝 Text #' + textlineId + ' → ' + (currentSectionId ? 'Section ' + currentSectionId : 'FREI')); debugLog(' ' + order + '. 📝 Text #' + textlineId + ' → ' + (textAssignToSection ? 'Section ' + textAssignToSection : 'FREI'));
order++; order++;
} }
} }
@ -697,59 +760,67 @@ function initCollapse() {
colorSections(); colorSections();
// NEU: Fehlende Subtotals einfügen // DEAKTIVIERT: JavaScript-Subtotal verursacht Duplikate
insertMissingSubtotals(); // insertLastSectionSubtotal();
debugLog('✅ Collapse initialisiert'); debugLog('✅ Collapse initialisiert');
} }
/** /**
* Fügt fehlende Subtotals am Ende ein (für letzte Section) * 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 insertMissingSubtotals() { function insertLastSectionSubtotal() {
debugLog('🔢 Prüfe fehlende Subtotals...'); debugLog('🔢 Prüfe Subtotal für letzte Section...');
$('tr.section-header').each(function() { var $allSections = $('tr.section-header');
var $header = $(this); if ($allSections.length === 0) {
var sectionId = $header.attr('data-section-id'); debugLog(' Keine Sections vorhanden');
var $checkbox = $header.find('.subtotal-toggle');
// Nur wenn Checkbox aktiviert ist
if (!$checkbox.length || !$checkbox.is(':checked')) {
return; return;
} }
// Finde letztes Produkt dieser Section // 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 + '"]'); var $products = $('tr[data-parent-section="' + sectionId + '"]');
if ($products.length === 0) { if ($products.length === 0) {
debugLog(' Letzte Section ' + sectionId + ': Keine Produkte');
return; return;
} }
var $lastProduct = $products.last(); var $lastProduct = $products.last();
// Prüfe ob direkt danach schon ein Subtotal kommt // Prüfe ob Subtotal für diese Section irgendwo im DOM existiert
var $nextRow = $lastProduct.next('tr'); // (sowohl data-section-id als auch data-subtotal-id prüfen)
if ($nextRow.hasClass('subtotal-row')) { var $existingSubtotal = $('tr.subtotal-row[data-section-id="' + sectionId + '"]');
debugLog(' Section ' + sectionId + ': Subtotal vorhanden ✓'); if ($existingSubtotal.length > 0) {
debugLog(' Letzte Section ' + sectionId + ': Subtotal existiert bereits im DOM ✓');
return; return;
} }
debugLog(' Section ' + sectionId + ': Subtotal fehlt - füge ein...'); // 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;
}
// Berechne Summe aus Produktzeilen debugLog(' Letzte Section ' + sectionId + ': Subtotal fehlt in DOM, hole Daten...');
// Berechne Summe lokal
var sum = 0; var sum = 0;
$products.each(function() { $products.each(function() {
var $row = $(this); var priceText = $(this).find('td.linecolht').text().trim();
// Versuche Netto-Betrag aus der Zeile zu holen
var priceText = $row.find('td.linecolht').text().trim();
if (!priceText) {
// Fallback: letzte Spalte mit Zahl
$row.find('td').each(function() {
var text = $(this).text().trim();
if (text.match(/^-?[\d\s.,]+$/)) {
priceText = text;
}
});
}
if (priceText) { if (priceText) {
var price = parseFloat(priceText.replace(/\s/g, '').replace('.', '').replace(',', '.')); var price = parseFloat(priceText.replace(/\s/g, '').replace('.', '').replace(',', '.'));
if (!isNaN(price)) { if (!isNaN(price)) {
@ -763,19 +834,66 @@ function insertMissingSubtotals() {
maximumFractionDigits: 2 maximumFractionDigits: 2
}); });
// Subtotal-Zeile einfügen // Hole Subtotal-ID aus Datenbank (oder erstelle ihn falls nötig)
var html = '<tr class="subtotal-row" data-section-id="' + sectionId + '" style="font-weight:bold;">'; var docType = getDocumentType();
html += '<td colspan="9" style="text-align:right; padding:8px;">Zwischensumme:</td>'; $.ajax({
html += '<td class="linecolht right" style="padding:8px;">' + formattedSum + '</td>'; 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="linecoledit"></td>';
html += '<td class="linecoldelete"></td>'; html += '<td class="linecoldelete"></td>';
html += '<td class="linecolmove"></td>'; html += '<td class="linecolmove"></td>';
html += '<td class="linecolunlink"></td>'; html += '<td class="linecolunlink"></td>';
html += '</tr>'; html += '</tr>';
$lastProduct.after(html); $afterElement.after(html);
debugLog(' → Subtotal eingefügt: ' + formattedSum + ' €'); debugLog(' → Subtotal eingefügt: ' + formattedSum + ' € (ID: ' + (subtotalId || 'keine') + ')');
});
} }
$(document).ready(function() { $(document).ready(function() {
@ -816,24 +934,26 @@ function insertTextLines() {
} }
function toggleSubtotal(sectionId, checkbox) { function toggleSubtotal(sectionId, checkbox) {
event.stopPropagation(); if (event) event.stopPropagation();
var show = checkbox.checked; var show = checkbox.checked;
var docType = getDocumentType();
debugLog('🔢 Toggle Subtotal für Section ' + sectionId + ': ' + show); debugLog('🔢 Toggle Subtotal für Section ' + sectionId + ': ' + show + ', docType: ' + docType);
$.ajax({ $.ajax({
url: '/dolibarr/custom/subtotaltitle/ajax/toggle_subtotal.php', url: '/dolibarr/custom/subtotaltitle/ajax/toggle_subtotal.php',
method: 'POST', method: 'POST',
data: { data: {
section_id: sectionId, section_id: sectionId,
show: show ? 1 : 0 show: show ? 1 : 0,
document_type: docType
}, },
dataType: 'json', dataType: 'json',
success: function(response) { success: function(response) {
debugLog('Subtotal Response: ' + JSON.stringify(response)); debugLog('Subtotal Response: ' + JSON.stringify(response));
if (response.success && response.reload) { if (response.success && response.reload) {
window.location.reload(); safeReload();
} }
}, },
error: function(xhr, status, error) { error: function(xhr, status, error) {
@ -1032,14 +1152,16 @@ function removeFromSection(productId) {
return; return;
} }
debugLog('🔓 Entferne Produkt ' + productId + ' aus Section'); var docType = getDocumentType();
debugLog('🔓 Entferne Produkt ' + productId + ' aus Section (docType: ' + docType + ')');
$.post('/dolibarr/custom/subtotaltitle/ajax/remove_from_section.php', { $.post('/dolibarr/custom/subtotaltitle/ajax/remove_from_section.php', {
product_id: productId product_id: productId,
document_type: docType
}, function(response) { }, function(response) {
debugLog('Remove response: ' + JSON.stringify(response)); debugLog('Remove response: ' + JSON.stringify(response));
if (response.success) { if (response.success) {
window.location.href = window.location.pathname + window.location.search; safeReload();
} else { } else {
alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler'));
} }
@ -1114,7 +1236,7 @@ function deleteMassSelected() {
facture_id: getFactureId() facture_id: getFactureId()
}, function(response) { }, function(response) {
if (response.success) { if (response.success) {
window.location.reload(); safeReload();
} else { } else {
alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannt')); alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannt'));
} }

View file

@ -135,6 +135,8 @@ function syncAllToFacturedet() {
return; return;
} }
var docType = getDocumentTypeForSync();
$unchecked.each(function() { $unchecked.each(function() {
var lineId = $(this).data('line-id'); var lineId = $(this).data('line-id');
var lineType = $(this).data('line-type'); var lineType = $(this).data('line-type');
@ -142,7 +144,8 @@ function syncAllToFacturedet() {
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', { $.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
action: 'add', action: 'add',
line_id: lineId, line_id: lineId,
line_type: lineType line_type: lineType,
document_type: docType
}, function(response) { }, function(response) {
done++; done++;
if (response.success) { if (response.success) {
@ -189,6 +192,8 @@ function removeAllFromFacturedet() {
return; return;
} }
var docType = getDocumentTypeForSync();
$checked.each(function() { $checked.each(function() {
var lineId = $(this).data('line-id'); var lineId = $(this).data('line-id');
var lineType = $(this).data('line-type'); var lineType = $(this).data('line-type');
@ -196,7 +201,8 @@ function removeAllFromFacturedet() {
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', { $.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
action: 'remove', action: 'remove',
line_id: lineId, line_id: lineId,
line_type: lineType line_type: lineType,
document_type: docType
}, function(response) { }, function(response) {
done++; done++;
if (response.success) { if (response.success) {
@ -237,12 +243,15 @@ function updateAllSubtotals() {
return; return;
} }
var docType = getDocumentTypeForSync();
$subtotals.each(function() { $subtotals.each(function() {
var lineId = $(this).data('line-id'); var lineId = $(this).data('line-id');
$.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', { $.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', {
action: 'update_subtotal', action: 'update_subtotal',
line_id: lineId line_id: lineId,
document_type: docType
}, function(response) { }, function(response) {
done++; done++;
debugLog('Subtotal #' + lineId + ' updated: ' + JSON.stringify(response)); debugLog('Subtotal #' + lineId + ' updated: ' + JSON.stringify(response));