diff --git a/ajax/assign_last_product.php b/ajax/assign_last_product.php index 4031bff..60a54e4 100755 --- a/ajax/assign_last_product.php +++ b/ajax/assign_last_product.php @@ -46,17 +46,40 @@ $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " WHERE ".$tables['fk_line']." = ".(int)$product_id; $resql = $db->query($sql); +// Hole die line_order der Section (Produkt soll direkt danach kommen) +$sql_section = "SELECT line_order FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE rowid = ".(int)$section_id; +$resql_section = $db->query($sql_section); +$section_order = 1; +if ($obj_section = $db->fetch_object($resql_section)) { + $section_order = $obj_section->line_order; +} + +// 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 - $next_order = 1; - $sql_max = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager"; - $sql_max .= " WHERE ".$tables['fk_parent']." = ".(int)$facture_id; - $sql_max .= " AND document_type = '".$db->escape($docType)."'"; - $resql_max = $db->query($sql_max); - if ($obj = $db->fetch_object($resql_max)) { - $next_order = ($obj->max_order ? $obj->max_order + 1 : 1); - } - // Setze alle FK-Felder explizit (NULL für nicht genutzte) $fk_facture = ($docType === 'invoice') ? (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 .= " (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); - 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 { - // Produkt existiert - UPDATE parent_section - subtotaltitle_debug_log('🔵🔵🔵 assign_last_product: Produkt #'.$product_id.' → parent_section='.$section_id); + // Produkt existiert - UPDATE parent_section UND line_order + 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 .= " SET parent_section = ".(int)$section_id; + $sql_upd .= ", line_order = ".$new_line_order; $sql_upd .= " WHERE ".$tables['fk_line']." = ".(int)$product_id; $db->query($sql_upd); - subtotaltitle_debug_log(' → parent_section updated'); + subtotaltitle_debug_log(' → parent_section und line_order updated'); } // Neu sortieren diff --git a/ajax/check_subtotal.php b/ajax/check_subtotal.php new file mode 100644 index 0000000..4c6b21a --- /dev/null +++ b/ajax/check_subtotal.php @@ -0,0 +1,48 @@ + 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 +]); diff --git a/ajax/cleanup_subtotals.php b/ajax/cleanup_subtotals.php new file mode 100644 index 0000000..5cfa1a5 --- /dev/null +++ b/ajax/cleanup_subtotals.php @@ -0,0 +1,144 @@ + 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 +]); diff --git a/ajax/create_section.php b/ajax/create_section.php index fd6ba05..5c46b65 100755 --- a/ajax/create_section.php +++ b/ajax/create_section.php @@ -40,12 +40,8 @@ $sql .= " VALUES (".$fk_facture.", ".$fk_propal.", ".$fk_commande.", '".$db->esc if ($db->query($sql)) { $section_id = $db->last_insert_id(MAIN_DB_PREFIX."facture_lines_manager"); - // Erstelle automatisch auch eine Zwischensumme für diese Section - $subtotal_order = $next_order + 1000; // Hohe Nummer, wird später normalisiert - $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); + // KEIN automatisches Subtotal mehr - wird nur erstellt wenn Checkbox aktiviert wird + // Das Subtotal wird über toggle_subtotal.php erstellt/gelöscht echo json_encode(['success' => true, 'section_id' => $section_id]); } else { diff --git a/ajax/fix_sections.php b/ajax/fix_sections.php new file mode 100644 index 0000000..0fcd4b6 --- /dev/null +++ b/ajax/fix_sections.php @@ -0,0 +1,58 @@ + 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' +]); diff --git a/ajax/get_sections.php b/ajax/get_sections.php index f036393..9f2fc36 100755 --- a/ajax/get_sections.php +++ b/ajax/get_sections.php @@ -22,7 +22,7 @@ if (!$tables) { // Hole ALLE Sections für diesen Dokumenttyp $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 .= " WHERE s.".$tables['fk_parent']." = ".(int)$facture_id; $sql .= " AND s.document_type = '".$db->escape($docType)."'"; diff --git a/ajax/move_section.php b/ajax/move_section.php index 976a97e..a0ef0d0 100755 --- a/ajax/move_section.php +++ b/ajax/move_section.php @@ -1,19 +1,41 @@ false, 'error' => 'Missing parameters')); 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 -$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 .= " AND line_type = 'section'"; +$sql .= " AND document_type = '".$db->escape($docType)."'"; $resql = $db->query($sql); if (!$resql || $db->num_rows($resql) == 0) { @@ -22,13 +44,14 @@ if (!$resql || $db->num_rows($resql) == 0) { } $section = $db->fetch_object($resql); -$facture_id = $section->fk_facture; +$doc_id = $section->doc_id; $db->begin(); // 1. Hole alle Sections (sortiert) $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 .= " ORDER BY line_order"; $resql = $db->query($sql); @@ -42,18 +65,21 @@ while ($obj = $db->fetch_object($resql)) { $current_index = array_search($section_id, $sections); if ($current_index === false) { + $db->rollback(); echo json_encode(array('success' => false, 'error' => 'Section not in list')); exit; } if ($direction == 'up') { if ($current_index == 0) { + $db->rollback(); echo json_encode(array('success' => false, 'error' => 'Already at top')); exit; } $swap_index = $current_index - 1; } else { if ($current_index == count($sections) - 1) { + $db->rollback(); echo json_encode(array('success' => false, 'error' => 'Already at bottom')); exit; } @@ -71,7 +97,8 @@ $updates = array(); // Freie Produkte zuerst $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 parent_section IS NULL"; $sql .= " ORDER BY line_order"; @@ -84,7 +111,8 @@ while ($obj = $db->fetch_object($resql)) { // Freie Textzeilen $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 parent_section IS NULL"; $sql .= " ORDER BY line_order"; @@ -100,46 +128,59 @@ foreach ($sections as $sec_id) { // Section-Header $updates[$sec_id] = $new_order; $new_order++; - + // Produkte dieser Section $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 parent_section = ".(int)$sec_id; $sql .= " ORDER BY line_order"; $resql = $db->query($sql); - + while ($obj = $db->fetch_object($resql)) { $updates[$obj->rowid] = $new_order; $new_order++; } - + // Textzeilen dieser Section $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 parent_section = ".(int)$sec_id; $sql .= " ORDER BY line_order"; $resql = $db->query($sql); - + while ($obj = $db->fetch_object($resql)) { $updates[$obj->rowid] = $new_order; $new_order++; } - - // ========== SUBTOTAL DIESER SECTION ========== + + // Subtotal dieser Section $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 parent_section = ".(int)$sec_id; $resql = $db->query($sql); - + while ($obj = $db->fetch_object($resql)) { $updates[$obj->rowid] = $new_order; $new_order++; } } +// 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 foreach ($updates as $rowid => $order) { $sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager"; @@ -148,22 +189,26 @@ foreach ($updates as $rowid => $order) { $db->query($sql); } -// 5. Sync rang -$sql = "SELECT fk_facturedet FROM ".MAIN_DB_PREFIX."facture_lines_manager"; -$sql .= " WHERE fk_facture = ".(int)$facture_id; -$sql .= " AND line_type = 'product'"; +// 5. Sync rang in Detail-Tabelle +$sql = "SELECT ".$tables['fk_line']." as detail_id FROM ".MAIN_DB_PREFIX."facture_lines_manager"; +$sql .= " WHERE ".$tables['fk_parent']." = ".(int)$doc_id; +$sql .= " AND document_type = '".$db->escape($docType)."'"; +$sql .= " AND ".$tables['fk_line']." IS NOT NULL"; $sql .= " ORDER BY line_order"; $resql = $db->query($sql); $rang = 1; while ($obj = $db->fetch_object($resql)) { - $sql_upd = "UPDATE ".MAIN_DB_PREFIX."facturedet"; - $sql_upd .= " SET rang = ".$rang; - $sql_upd .= " WHERE rowid = ".(int)$obj->fk_facturedet; - $db->query($sql_upd); - $rang++; + $detail_id = $obj->detail_id; + if ($detail_id) { + $sql_upd = "UPDATE ".MAIN_DB_PREFIX.$tables['lines_table']; + $sql_upd .= " SET rang = ".$rang; + $sql_upd .= " WHERE rowid = ".(int)$detail_id; + $db->query($sql_upd); + $rang++; + } } $db->commit(); -echo json_encode(array('success' => true)); \ No newline at end of file +echo json_encode(array('success' => true)); diff --git a/ajax/remove_from_section.php b/ajax/remove_from_section.php index 67383c0..f3e25bb 100755 --- a/ajax/remove_from_section.php +++ b/ajax/remove_from_section.php @@ -2,24 +2,35 @@ define('NOTOKENRENEWAL', 1); require '../../../main.inc.php'; require_once __DIR__.'/../lib/subtotaltitle.lib.php'; +require_once __DIR__.'/../class/DocumentTypeHelper.class.php'; $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) { - echo json_encode(['success' => false, 'error' => 'Missing product_id']); +if (!$product_id || !$docType) { + 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; } $sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager"; $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); if ($result) { + subtotaltitle_debug_log('✅ Produkt #' . $product_id . ' aus Section entfernt'); echo json_encode(['success' => true]); } else { echo json_encode(['success' => false, 'error' => $db->lasterror()]); -} \ No newline at end of file +} diff --git a/ajax/reorder_all.php b/ajax/reorder_all.php index 982dd1d..9010127 100755 --- a/ajax/reorder_all.php +++ b/ajax/reorder_all.php @@ -62,30 +62,39 @@ foreach ($new_order as $item) { // ========== SUBTOTALS NEU POSITIONIEREN ========== subtotaltitle_debug_log('🔢 Repositioniere Subtotals...'); +// Hole alle Subtotals für dieses Dokument $sql = "SELECT rowid, parent_section FROM ".MAIN_DB_PREFIX."facture_lines_manager WHERE ".$tables['fk_parent']." = ".(int)$facture_id." AND document_type = '".$db->escape($docType)."' AND line_type = 'subtotal'"; $resql = $db->query($sql); +$subtotals_to_update = array(); 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 $sql_max = "SELECT MAX(line_order) as max_order FROM ".MAIN_DB_PREFIX."facture_lines_manager 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); $obj_max = $db->fetch_object($res_max); - + if ($obj_max && $obj_max->max_order) { - // Subtotal bekommt hohe Nummer (wird gleich normalisiert) - $temp_order = (int)$obj_max->max_order * 100 + 50; - + // Subtotal kommt direkt nach dem letzten Produkt: max_order + 0.5 + $temp_order = (float)$obj_max->max_order + 0.5; + $sql_upd = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager SET line_order = ".$temp_order." WHERE rowid = ".(int)$subtotal->rowid; $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); } } diff --git a/class/actions_subtotaltitle.class.php b/class/actions_subtotaltitle.class.php index 6f46d77..e332e7b 100755 --- a/class/actions_subtotaltitle.class.php +++ b/class/actions_subtotaltitle.class.php @@ -518,10 +518,8 @@ class ActionsSubtotalTitle extends CommonHookActions // Prüfe ob diese Zeile eine unserer speziellen Zeilen ist (Section, Text, Subtotal) // special_code: 100=Section, 101=Text, 102=Subtotal if (isset($line->special_code) && in_array($line->special_code, array(100, 101, 102))) { - // Diese Zeile ist eine unserer speziellen Zeilen - per JS ausblenden - echo ''; + // Diese Zeile wird von uns selbst gerendert - Original sofort per CSS verstecken + echo ''; return 0; } @@ -598,23 +596,31 @@ class ActionsSubtotalTitle extends CommonHookActions $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) { - $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)$last_parent_section[$doc_key]; - $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])) { - echo $this->renderSubtotalLine($obj_sub); - self::$rendered_sections[$doc_key][] = $subtotal_key; - - if ($this->debug) { - error_log('[SubtotalTitle] ✅ Subtotal "'.$obj_sub->title.'" gerendert (Section-Wechsel)'); + // 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 .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id; + $sql_subtotal .= " AND document_type = '".$db->escape($docType)."'"; + $sql_subtotal .= " AND parent_section = ".(int)$last_parent_section[$doc_key]; + $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])) { + echo $this->renderSubtotalLine($obj_sub); + self::$rendered_sections[$doc_key][] = $subtotal_key; + + if ($this->debug) { + error_log('[SubtotalTitle] ✅ Subtotal "'.$obj_sub->title.'" gerendert (Section-Wechsel)'); + } } } } @@ -682,6 +688,55 @@ class ActionsSubtotalTitle extends CommonHookActions // Merke für nächsten Durchlauf $last_rang[$doc_key] = $current_rang; $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 ''; + self::$rendered_sections[$doc_key][] = $subtotal_key; + + if ($this->debug) { + error_log('[SubtotalTitle] ✅ Subtotal für letzte Section per JS eingefügt'); + } + } + } + } + } + } } /** @@ -839,29 +894,40 @@ class ActionsSubtotalTitle extends CommonHookActions $sql_cleanup .= " AND d.rowid IS NULL"; $result = $db->query($sql_cleanup); - // 2. Hole alle Produktzeilen des Dokuments - $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX.$tables['lines_table']; + // 2. Hole alle Produktzeilen des Dokuments mit rang + $sql = "SELECT rowid, rang FROM ".MAIN_DB_PREFIX.$tables['lines_table']; $sql .= " WHERE ".$tables['fk_parent']." = ".(int)$document_id; $sql .= " ORDER BY rang"; $resql = $db->query($sql); + $new_products = array(); while ($obj = $db->fetch_object($resql)) { $sql_check = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_lines_manager"; $sql_check .= " WHERE ".$tables['fk_line']." = ".(int)$obj->rowid; $resql_check = $db->query($sql_check); if ($db->num_rows($resql_check) == 0) { - $next_order = $this->getNextLineOrder($document_id, $docType); + // Neues Produkt gefunden - merken mit rang + $new_products[] = array('rowid' => $obj->rowid, 'rang' => $obj->rang); + } + } - // Setze alle FK-Felder explizit (NULL für nicht genutzte) - $fk_facture = ($docType === 'invoice') ? (int)$document_id : 'NULL'; - $fk_propal = ($docType === 'propal') ? (int)$document_id : 'NULL'; - $fk_commande = ($docType === 'order') ? (int)$document_id : 'NULL'; + // 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); + // Setze alle FK-Felder explizit (NULL für nicht genutzte) + $fk_facture = ($docType === 'invoice') ? (int)$document_id : 'NULL'; + $fk_propal = ($docType === 'propal') ? (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 .= " (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); + $next_order++; // Für jedes weitere Produkt erhöhen } } } @@ -1202,9 +1268,12 @@ class ActionsSubtotalTitle extends CommonHookActions global $db; $tables = DocumentTypeHelper::getTableNames($docType); + if (!$tables) return 1; + $sql = "SELECT MAX(line_order) as max_order"; $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); if (!$resql || !($obj = $db->fetch_object($resql))) { return 1; diff --git a/js/subtotaltitle.js b/js/subtotaltitle.js index 478ac60..06ed433 100755 --- a/js/subtotaltitle.js +++ b/js/subtotaltitle.js @@ -1,5 +1,5 @@ // DEBUG FLAG - true für Debug-Ausgaben, false für Produktiv -var SUBTOTAL_DEBUG = true; +var SUBTOTAL_DEBUG = false; function debugLog(message) { 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? var isDragging = false; var isTogglingSubtotal = false; @@ -20,7 +50,8 @@ if (typeof SubtotalTitleLoaded === 'undefined') { // Füge Button zu den Standard-Buttons hinzu - NUR im Entwurfsstatus if ($('#tablelines').length > 0) { - var factureId = getFactureId(); + // Cleanup verwaister Subtotals (wo show_subtotal=0) + cleanupOrphanedSubtotals(); // Prüfe ob Dokument im Entwurfsstatus ist if (typeof subtotalTitleIsDraft !== 'undefined' && subtotalTitleIsDraft === true) { @@ -34,7 +65,6 @@ if (typeof SubtotalTitleLoaded === 'undefined') { debugLog('⚠️ Dokument nicht im Entwurfsstatus - Button wird nicht angezeigt'); } - // ⬇️ HIER FEHLTE DER AUFRUF! ⬇️ initDragAndDrop(); } }); @@ -104,11 +134,13 @@ function createNewSection() { */ function moveSection(sectionId, direction) { 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', { section_id: sectionId, - direction: direction + direction: direction, + document_type: docInfo.type }, function(response) { debugLog('Move response: ' + JSON.stringify(response)); if (response.success) { @@ -401,25 +433,56 @@ function saveCurrentOrder() { 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: currentSectionId + parent_section: assignToSection }); - debugLog(' ' + order + '. 📦 Produkt #' + productId + ' → ' + (currentSectionId ? 'Section ' + currentSectionId : 'FREI')); + 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: currentSectionId + parent_section: textAssignToSection }); - debugLog(' ' + order + '. 📝 Text #' + textlineId + ' → ' + (currentSectionId ? 'Section ' + currentSectionId : 'FREI')); + debugLog(' ' + order + '. 📝 Text #' + textlineId + ' → ' + (textAssignToSection ? 'Section ' + textAssignToSection : 'FREI')); order++; } } @@ -696,86 +759,141 @@ function initCollapse() { }); colorSections(); - - // NEU: Fehlende Subtotals einfügen - insertMissingSubtotals(); + + // DEAKTIVIERT: JavaScript-Subtotal verursacht Duplikate + // insertLastSectionSubtotal(); 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() { - debugLog('🔢 Prüfe fehlende Subtotals...'); - - $('tr.section-header').each(function() { - var $header = $(this); - var sectionId = $header.attr('data-section-id'); - var $checkbox = $header.find('.subtotal-toggle'); - - // Nur wenn Checkbox aktiviert ist - if (!$checkbox.length || !$checkbox.is(':checked')) { - return; - } - - // Finde letztes Produkt dieser Section - var $products = $('tr[data-parent-section="' + sectionId + '"]'); - if ($products.length === 0) { - return; - } - - var $lastProduct = $products.last(); - - // Prüfe ob direkt danach schon ein Subtotal kommt - var $nextRow = $lastProduct.next('tr'); - if ($nextRow.hasClass('subtotal-row')) { - debugLog(' Section ' + sectionId + ': Subtotal vorhanden ✓'); - return; - } - - debugLog(' Section ' + sectionId + ': Subtotal fehlt - füge ein...'); - - // Berechne Summe aus Produktzeilen - var sum = 0; - $products.each(function() { - var $row = $(this); - // 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; - } - }); +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; } - 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 - }); - - // Subtotal-Zeile einfügen - var html = ''; - html += 'Zwischensumme:'; - html += '' + formattedSum + ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - - $lastProduct.after(html); - debugLog(' → Subtotal eingefügt: ' + formattedSum + ' €'); + } }); + + var formattedSum = sum.toLocaleString('de-DE', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + + // Hole Subtotal-ID aus Datenbank (oder erstelle ihn falls nötig) + var docType = getDocumentType(); + $.ajax({ + url: '/dolibarr/custom/subtotaltitle/ajax/check_subtotal.php', + method: 'GET', + data: { + section_id: sectionId, + document_type: docType + }, + dataType: 'json', + success: function(response) { + if (response.exists && response.subtotal_id) { + // Subtotal existiert - zeige ihn mit Checkbox an + renderSubtotalRow($lastProduct, sectionId, response.subtotal_id, response.in_facturedet, formattedSum); + } else { + // Subtotal existiert nicht in DB - zeige ohne Checkbox (nur Vorschau) + renderSubtotalRow($lastProduct, sectionId, null, false, formattedSum); + } + }, + error: function() { + // Bei Fehler: zeige Subtotal ohne Checkbox + renderSubtotalRow($lastProduct, sectionId, null, false, formattedSum); + } + }); +} + +/** + * Rendert eine Subtotal-Zeile im DOM + */ +function renderSubtotalRow($afterElement, sectionId, subtotalId, inFacturedet, formattedSum) { + var inClass = inFacturedet ? ' in-facturedet' : ''; + var syncChecked = inFacturedet ? 'checked' : ''; + + var html = ''; + html += ' 📄'; + } + + html += ''; + html += '' + formattedSum + ' €'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + $afterElement.after(html); + debugLog(' → Subtotal eingefügt: ' + formattedSum + ' € (ID: ' + (subtotalId || 'keine') + ')'); } $(document).ready(function() { @@ -816,24 +934,26 @@ function insertTextLines() { } function toggleSubtotal(sectionId, checkbox) { - event.stopPropagation(); - + if (event) event.stopPropagation(); + var show = checkbox.checked; - - debugLog('🔢 Toggle Subtotal für Section ' + sectionId + ': ' + show); - + var docType = getDocumentType(); + + debugLog('🔢 Toggle Subtotal für Section ' + sectionId + ': ' + show + ', docType: ' + docType); + $.ajax({ url: '/dolibarr/custom/subtotaltitle/ajax/toggle_subtotal.php', method: 'POST', data: { section_id: sectionId, - show: show ? 1 : 0 + show: show ? 1 : 0, + document_type: docType }, dataType: 'json', success: function(response) { debugLog('Subtotal Response: ' + JSON.stringify(response)); if (response.success && response.reload) { - window.location.reload(); + safeReload(); } }, error: function(xhr, status, error) { @@ -1032,14 +1152,16 @@ function removeFromSection(productId) { 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', { - product_id: productId + product_id: productId, + document_type: docType }, function(response) { debugLog('Remove response: ' + JSON.stringify(response)); if (response.success) { - window.location.href = window.location.pathname + window.location.search; + safeReload(); } else { alert((lang.errorReordering || 'Fehler') + ': ' + (response.error || 'Unbekannter Fehler')); } @@ -1114,7 +1236,7 @@ function deleteMassSelected() { facture_id: getFactureId() }, function(response) { if (response.success) { - window.location.reload(); + safeReload(); } else { alert((lang.errorDeletingSection || 'Fehler') + ': ' + (response.error || 'Unbekannt')); } diff --git a/js/subtotaltitle_sync.js b/js/subtotaltitle_sync.js index da3a813..33cb27e 100755 --- a/js/subtotaltitle_sync.js +++ b/js/subtotaltitle_sync.js @@ -135,6 +135,8 @@ function syncAllToFacturedet() { return; } + var docType = getDocumentTypeForSync(); + $unchecked.each(function() { var lineId = $(this).data('line-id'); var lineType = $(this).data('line-type'); @@ -142,7 +144,8 @@ function syncAllToFacturedet() { $.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', { action: 'add', line_id: lineId, - line_type: lineType + line_type: lineType, + document_type: docType }, function(response) { done++; if (response.success) { @@ -189,6 +192,8 @@ function removeAllFromFacturedet() { return; } + var docType = getDocumentTypeForSync(); + $checked.each(function() { var lineId = $(this).data('line-id'); var lineType = $(this).data('line-type'); @@ -196,7 +201,8 @@ function removeAllFromFacturedet() { $.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', { action: 'remove', line_id: lineId, - line_type: lineType + line_type: lineType, + document_type: docType }, function(response) { done++; if (response.success) { @@ -237,12 +243,15 @@ function updateAllSubtotals() { return; } + var docType = getDocumentTypeForSync(); + $subtotals.each(function() { var lineId = $(this).data('line-id'); - + $.post('/dolibarr/custom/subtotaltitle/ajax/sync_to_facturedet.php', { action: 'update_subtotal', - line_id: lineId + line_id: lineId, + document_type: docType }, function(response) { done++; debugLog('Subtotal #' + lineId + ' updated: ' + JSON.stringify(response));