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 ".$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) { echo json_encode(array('success' => false, 'error' => 'Section not found')); exit; } $section = $db->fetch_object($resql); $doc_id = $section->doc_id; $db->begin(); // 1. Hole alle Sections (sortiert) $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 = 'section'"; $sql .= " ORDER BY line_order"; $resql = $db->query($sql); $sections = array(); while ($obj = $db->fetch_object($resql)) { $sections[] = $obj->rowid; } // 2. Finde Index und tausche $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; } $swap_index = $current_index + 1; } // Tausche $temp = $sections[$current_index]; $sections[$current_index] = $sections[$swap_index]; $sections[$swap_index] = $temp; // 3. Baue komplette neue Reihenfolge auf // Strategie: Freie Zeilen behalten ihre Position relativ zu Sections $new_order = 1; $updates = array(); $processed = array(); // Hole die aktuelle line_order der ersten Section (VOR dem Swap) $sql = "SELECT MIN(line_order) as min_section_order 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 = 'section'"; $resql = $db->query($sql); $obj = $db->fetch_object($resql); $first_section_order = $obj ? $obj->min_section_order : 9999; // 1. FREIE ZEILEN VOR allen Sections (line_order < erste Section) $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 (parent_section IS NULL OR parent_section = 0)"; $sql .= " AND line_type != 'section'"; $sql .= " AND line_order < ".(int)$first_section_order; $sql .= " ORDER BY line_order"; $resql = $db->query($sql); while ($obj = $db->fetch_object($resql)) { $updates[$obj->rowid] = $new_order; $processed[$obj->rowid] = true; dol_syslog('[SubtotalTitle] move_section: Freie Zeile VOR Sections #'.$obj->rowid.' → line_order='.$new_order, LOG_INFO); $new_order++; } // 2. SECTIONS in neuer Reihenfolge (nach dem Swap) foreach ($sections as $sec_id) { // Section-Header $updates[$sec_id] = $new_order; $processed[$sec_id] = true; dol_syslog('[SubtotalTitle] move_section: Section #'.$sec_id.' → line_order='.$new_order, LOG_INFO); $new_order++; // Produkte dieser Section $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 = ".(int)$sec_id; $sql .= " ORDER BY line_order"; $resql = $db->query($sql); while ($obj = $db->fetch_object($resql)) { $updates[$obj->rowid] = $new_order; $processed[$obj->rowid] = true; $new_order++; } // Textzeilen dieser Section $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 = '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; $processed[$obj->rowid] = true; $new_order++; } // Subtotal dieser Section $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 = 'subtotal'"; $sql .= " AND parent_section = ".(int)$sec_id; $resql = $db->query($sql); while ($obj = $db->fetch_object($resql)) { $updates[$obj->rowid] = $new_order; $processed[$obj->rowid] = true; $new_order++; } } // 3. FREIE ZEILEN NACH allen Sections (die noch nicht processed wurden) $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 (parent_section IS NULL OR parent_section = 0)"; $sql .= " AND line_type != 'section'"; $sql .= " ORDER BY line_order"; $resql = $db->query($sql); while ($obj = $db->fetch_object($resql)) { if (!isset($processed[$obj->rowid])) { $updates[$obj->rowid] = $new_order; $processed[$obj->rowid] = true; dol_syslog('[SubtotalTitle] move_section: Freie Zeile NACH Sections #'.$obj->rowid.' → line_order='.$new_order, LOG_INFO); $new_order++; } } // 4. Führe alle Updates aus foreach ($updates as $rowid => $order) { $sql = "UPDATE ".MAIN_DB_PREFIX."facture_lines_manager"; $sql .= " SET line_order = ".(int)$order; $sql .= " WHERE rowid = ".(int)$rowid; $db->query($sql); } // 5. Sync rang in Detail-Tabelle dol_syslog('[SubtotalTitle] move_section: Starte rang-Synchronisation für docType='.$docType.' doc_id='.$doc_id, LOG_INFO); $sql = "SELECT rowid, line_type, ".$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)) { $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); dol_syslog('[SubtotalTitle] Sync rang: '.$obj->line_type.' manager#'.$obj->rowid.' detail#'.$detail_id.' → rang='.$rang, LOG_INFO); $rang++; } } dol_syslog('[SubtotalTitle] move_section: rang-Synchronisation abgeschlossen, '.$rang.' Zeilen synchronisiert', LOG_INFO); $db->commit(); echo json_encode(array('success' => true));