diff --git a/src/db_connect.php b/src/db_connect.php
index a565767..d70316f 100644
--- a/src/db_connect.php
+++ b/src/db_connect.php
@@ -39,4 +39,21 @@ $conn->set_charset("utf8mb4");
// Zeitzone setzen
date_default_timezone_set('Europe/Berlin');
+
+// --- AUTO MIGRATIONS ---
+// Ensure display_name exists
+$check_display_name = $conn->query("SHOW COLUMNS FROM users LIKE 'display_name'");
+if ($check_display_name && $check_display_name->num_rows == 0) {
+ $conn->query("ALTER TABLE users ADD COLUMN display_name VARCHAR(255) DEFAULT NULL");
+}
+
+// Ensure todo_lists and todo_items exist
+$conn->query("CREATE TABLE IF NOT EXISTS todo_lists (id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, household_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
+$conn->query("CREATE TABLE IF NOT EXISTS todo_items (id INT AUTO_INCREMENT PRIMARY KEY, todo_list_id INT NOT NULL, title VARCHAR(255) NOT NULL, is_completed TINYINT(1) DEFAULT 0, order_index INT DEFAULT 0, FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
+
+$check_todo_list_id = $conn->query("SHOW COLUMNS FROM packing_lists LIKE 'todo_list_id'");
+if ($check_todo_list_id && $check_todo_list_id->num_rows == 0) {
+ $conn->query("ALTER TABLE packing_lists ADD COLUMN todo_list_id INT DEFAULT NULL");
+ $conn->query("ALTER TABLE packing_lists ADD CONSTRAINT fk_packing_list_todo FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE SET NULL");
+}
?>
diff --git a/src/index.php b/src/index.php
index 02a8b6c..4c0f224 100644
--- a/src/index.php
+++ b/src/index.php
@@ -13,21 +13,6 @@ if (!isset($_SESSION['user_id'])) {
require_once 'db_connect.php';
-// --- AUTO MIGRATIONS ---
-$check_display_name = $conn->query("SHOW COLUMNS FROM users LIKE 'display_name'");
-if ($check_display_name && $check_display_name->num_rows == 0) {
- $conn->query("ALTER TABLE users ADD COLUMN display_name VARCHAR(255) DEFAULT NULL");
-}
-
-$conn->query("CREATE TABLE IF NOT EXISTS todo_lists (id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, household_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
-$conn->query("CREATE TABLE IF NOT EXISTS todo_items (id INT AUTO_INCREMENT PRIMARY KEY, todo_list_id INT NOT NULL, title VARCHAR(255) NOT NULL, is_completed TINYINT(1) DEFAULT 0, order_index INT DEFAULT 0, FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
-
-$check_todo_list_id = $conn->query("SHOW COLUMNS FROM packing_lists LIKE 'todo_list_id'");
-if ($check_todo_list_id && $check_todo_list_id->num_rows == 0) {
- $conn->query("ALTER TABLE packing_lists ADD COLUMN todo_list_id INT DEFAULT NULL");
- $conn->query("ALTER TABLE packing_lists ADD CONSTRAINT fk_packing_list_todo FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE SET NULL");
-}
-
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];
diff --git a/src/packing_list_detail.php b/src/packing_list_detail.php
index e69de29..4ce2381 100644
--- a/src/packing_list_detail.php
+++ b/src/packing_list_detail.php
@@ -0,0 +1,635 @@
+prepare("UPDATE todo_items SET is_completed = ? WHERE id = ?");
+ $stmt->bind_param("ii", $status, $item_id);
+ $stmt->execute();
+ $stmt->close();
+ // Redirect to avoid form resubmission
+ header("Location: packing_list_detail.php?id=" . $packing_list_id);
+ exit;
+}
+
+require_once 'header.php';
+
+$packing_list = null;
+$total_weight_grams = 0;
+$total_consumable_weight = 0;
+$weight_by_category = [];
+$weight_by_carrier = [];
+$weight_by_carrier_non_consumable = [];
+
+// Array for Carrier Detailed Stats (Modal)
+$carrier_stats_details = [];
+
+if ($packing_list_id <= 0) {
+ die("Keine Packlisten-ID angegeben.");
+}
+
+// Erweiterte Berechtigungsprüfung für Haushalte
+$stmt_current_user = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
+$stmt_current_user->bind_param("i", $current_user_id);
+$stmt_current_user->execute();
+$current_user_household_id = $stmt_current_user->get_result()->fetch_assoc()['household_id'];
+$stmt_current_user->close();
+
+$stmt_list_owner = $conn->prepare(
+ "SELECT pl.*, u.household_id as owner_household_id
+ FROM packing_lists pl
+ JOIN users u ON pl.user_id = u.id
+ WHERE pl.id = ?"
+);
+$stmt_list_owner->bind_param("i", $packing_list_id);
+$stmt_list_owner->execute();
+$result = $stmt_list_owner->get_result();
+if ($result->num_rows > 0) {
+ $packing_list = $result->fetch_assoc();
+ $is_owner = ($packing_list['user_id'] == $current_user_id);
+ $is_household_list = !empty($packing_list['household_id']);
+ $is_in_same_household = ($is_household_list && $packing_list['household_id'] == $current_user_household_id);
+
+ if (!$is_owner && !$is_in_same_household) {
+ die("Packliste nicht gefunden oder Zugriff verweigert.");
+ }
+} else {
+ die("Packliste nicht gefunden oder Zugriff verweigert.");
+}
+$stmt_list_owner->close();
+
+$todo_items = [];
+$todo_list_name = '';
+if (!empty($packing_list['todo_list_id'])) {
+ $stmt_tl = $conn->prepare("SELECT name FROM todo_lists WHERE id = ?");
+ $stmt_tl->bind_param("i", $packing_list['todo_list_id']);
+ $stmt_tl->execute();
+ $res_tl = $stmt_tl->get_result();
+ if ($row = $res_tl->fetch_assoc()) $todo_list_name = $row['name'];
+ $stmt_tl->close();
+
+ $stmt_td = $conn->prepare("SELECT * FROM todo_items WHERE todo_list_id = ? ORDER BY is_completed ASC, id ASC");
+ $stmt_td->bind_param("i", $packing_list['todo_list_id']);
+ $stmt_td->execute();
+ $todo_items = $stmt_td->get_result()->fetch_all(MYSQLI_ASSOC);
+ $stmt_td->close();
+}
+
+// Check for items on the table
+$stmt_table = $conn->prepare("SELECT COUNT(*) as table_count FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id IS NULL AND backpack_id IS NULL AND backpack_compartment_id IS NULL AND parent_packing_list_item_id IS NULL");
+$stmt_table->bind_param("i", $packing_list_id);
+$stmt_table->execute();
+$table_items_count = $stmt_table->get_result()->fetch_assoc()['table_count'];
+$stmt_table->close();
+
+$page_title = "Packliste: " . htmlspecialchars($packing_list['name']);
+
+// FIX: Join Categories also for Backpacks
+$sql = "SELECT
+ pli.id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id,
+ pli.backpack_id, pli.backpack_compartment_id,
+ COALESCE(a.name, pli.name, bp.name, bpc.name, 'Unbekanntes Item') AS article_name,
+ COALESCE(a.weight_grams, bp.weight_grams, 0) as weight_grams,
+ a.image_url, a.product_designation, a.consumable,
+ COALESCE(c.name, c_bp.name, 'Sonstiges') AS category_name,
+ m.name AS manufacturer_name,
+ COALESCE(NULLIF(u.display_name, ''), u.username) AS carrier_name,
+ u.id AS carrier_id
+ FROM packing_list_items AS pli
+ LEFT JOIN articles AS a ON pli.article_id = a.id
+ LEFT JOIN backpacks AS bp ON pli.backpack_id = bp.id
+ LEFT JOIN backpack_compartments AS bpc ON pli.backpack_compartment_id = bpc.id
+ LEFT JOIN categories AS c ON a.category_id = c.id
+ LEFT JOIN categories AS c_bp ON bp.category_id = c_bp.id
+ LEFT JOIN manufacturers AS m ON a.manufacturer_id = m.id
+ LEFT JOIN users AS u ON pli.carrier_user_id = u.id
+ WHERE pli.packing_list_id = ?
+ AND NOT (pli.carrier_user_id IS NULL AND pli.backpack_id IS NULL AND pli.backpack_compartment_id IS NULL AND pli.parent_packing_list_item_id IS NULL)
+ ORDER BY pli.order_index ASC";
+
+$stmt = $conn->prepare($sql);
+if (!$stmt) {
+ die("SQL Fehler: " . $conn->error);
+}
+$stmt->bind_param("i", $packing_list_id);
+$stmt->execute();
+$result = $stmt->get_result();
+
+$items_by_id = [];
+$items_by_parent = [];
+
+while ($row = $result->fetch_assoc()) {
+ $items_by_id[$row['id']] = $row;
+ $parent_id = $row['parent_packing_list_item_id'] ?: 0;
+ if (!isset($items_by_parent[$parent_id])) {
+ $items_by_parent[$parent_id] = [];
+ }
+ $items_by_parent[$parent_id][] = $row;
+
+ // Stats
+ $item_weight = $row['quantity'] * $row['weight_grams'];
+ $total_weight_grams += $item_weight;
+
+ $carrier_name = $row['carrier_name'] ?: 'Sonstiges';
+ $carrier_id = $row['carrier_id'] ?: 0;
+
+ // Init stats arrays
+ if (!isset($weight_by_carrier[$carrier_name])) $weight_by_carrier[$carrier_name] = 0;
+ $weight_by_carrier[$carrier_name] += $item_weight;
+
+ $cat_name = $row['category_name'] ?: 'Sonstiges';
+ if (!isset($weight_by_category[$cat_name])) $weight_by_category[$cat_name] = 0;
+ $weight_by_category[$cat_name] += $item_weight;
+
+ if ($row['consumable']) {
+ $total_consumable_weight += $item_weight;
+ } else {
+ if (!isset($weight_by_carrier_non_consumable[$carrier_name])) $weight_by_carrier_non_consumable[$carrier_name] = 0;
+ $weight_by_carrier_non_consumable[$carrier_name] += $item_weight;
+ }
+
+ // Prepare Detailed Stats per Carrier for Modal
+ if (!isset($carrier_stats_details[$carrier_name])) {
+ $carrier_stats_details[$carrier_name] = [
+ 'total_weight' => 0,
+ 'base_weight' => 0,
+ 'consumable_weight' => 0,
+ 'categories' => []
+ ];
+ }
+ $carrier_stats_details[$carrier_name]['total_weight'] += $item_weight;
+ if ($row['consumable']) {
+ $carrier_stats_details[$carrier_name]['consumable_weight'] += $item_weight;
+ } else {
+ $carrier_stats_details[$carrier_name]['base_weight'] += $item_weight;
+ }
+ if (!isset($carrier_stats_details[$carrier_name]['categories'][$cat_name])) {
+ $carrier_stats_details[$carrier_name]['categories'][$cat_name] = 0;
+ }
+ $carrier_stats_details[$carrier_name]['categories'][$cat_name] += $item_weight;
+}
+$stmt->close();
+$conn->close();
+
+$total_weight_without_consumables = $total_weight_grams - $total_consumable_weight;
+
+// Helper functions (same as before)
+function get_recursive_quantity($parent_id, $items_by_parent) {
+ $count = 0;
+ if (isset($items_by_parent[$parent_id])) {
+ foreach ($items_by_parent[$parent_id] as $child) {
+ $count += $child['quantity'];
+ $count += get_recursive_quantity($child['id'], $items_by_parent);
+ }
+ }
+ return $count;
+}
+
+function get_recursive_weight($parent_id, $items_by_parent) {
+ $weight = 0;
+ if (isset($items_by_parent[$parent_id])) {
+ foreach ($items_by_parent[$parent_id] as $child) {
+ $weight += ($child['quantity'] * $child['weight_grams']);
+ $weight += get_recursive_weight($child['id'], $items_by_parent);
+ }
+ }
+ return $weight;
+}
+
+function render_item_row($item, $level, $items_by_parent) {
+ $has_children = isset($items_by_parent[$item['id']]);
+ $is_backpack = !empty($item['backpack_id']);
+ $is_compartment = !empty($item['backpack_compartment_id']);
+
+ $bg_class = "";
+ $text_class = "";
+ $icon = "";
+
+ if ($is_backpack) {
+ $bg_class = "table-success";
+ $text_class = "fw-bold text-uppercase";
+ $icon = ' ';
+ } elseif ($is_compartment) {
+ $bg_class = "table-light";
+ $text_class = "fw-bold fst-italic text-muted";
+ // Check if linked article (image present)
+ if (!empty($item['image_url'])) {
+ $img_src = htmlspecialchars($item['image_url']);
+ $icon = ' ';
+ } else {
+ $icon = ' ';
+ }
+ } else {
+ $img_src = !empty($item['image_url']) ? htmlspecialchars($item['image_url']) : 'assets/images/keinbild.png';
+ $icon = ' ';
+ }
+
+ $indent_px = $level * 25;
+ $weight_display = $item['weight_grams'] > 0 ? number_format($item['weight_grams'], 0, ',', '.') . ' g' : '-';
+
+ $total_weight_val = 0;
+ if ($is_backpack || $is_compartment) {
+ $children_weight = get_recursive_weight($item['id'], $items_by_parent);
+ $own_weight = $item['quantity'] * $item['weight_grams'];
+ $total_weight_val = $own_weight + $children_weight;
+ } else {
+ $total_weight_val = $item['weight_grams'] * $item['quantity'];
+ }
+
+ $total_weight_display = ($total_weight_val > 0) ? number_format($total_weight_val, 0, ',', '.') . ' g' : '-';
+
+ echo '
';
+
+ echo '';
+ echo '';
+
+ if ($has_children) {
+ echo ' ';
+ } else {
+ echo ' ';
+ }
+
+ echo $icon;
+ echo '' . htmlspecialchars($item['article_name']) . ' ';
+ echo '
';
+ echo ' ';
+
+ echo '' . ($item['consumable'] ? ' ' : '') . ' ';
+ echo '' . htmlspecialchars($item['manufacturer_name'] ?: '') . ' ';
+ echo '' . htmlspecialchars($item['product_designation'] ?: '') . ' ';
+ echo '' . htmlspecialchars($item['category_name'] ?: '') . ' ';
+
+ echo '';
+ if ($is_backpack) {
+ echo '';
+ } elseif ($is_compartment) {
+ $total_items = get_recursive_quantity($item['id'], $items_by_parent);
+ if ($total_items > 0) {
+ echo '' . $total_items . ' ';
+ }
+ } else {
+ echo '' . $item['quantity'] . 'x ';
+ }
+ echo ' ';
+
+ echo '' . $weight_display . ' ';
+ echo '' . $total_weight_display . ' ';
+
+ echo ' ';
+
+ if ($has_children) {
+ foreach ($items_by_parent[$item['id']] as $child) {
+ render_item_row($child, $level + 1, $items_by_parent);
+ }
+ }
+}
+?>
+
+
+
+
+
+
+
+
+ 0): ?>
+
+
Achtung: Auf dem Tisch liegen noch Artikel, die keinem Rucksack und keinem Träger zugewiesen sind! Diese fließen nicht in die Statistik oder den Druck ein.
+
+
Jetzt zuweisen
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Artikel / Struktur
+
+ Hersteller
+ Modell
+ Kategorie
+ Anz.
+ Gewicht
+ Gesamt
+
+
+
+
+ Liste ist leer.
+
+ $roots): ?>
+
+
+
+
+
+ (Klicken für Statistik)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Keine Einträge in dieser Liste.
+
+
+
+
+
+
+
+
+
+
+
Gewicht pro Träger
+
+ $weight): ?>
+
+
+
+ Gesamt: g
+
+
+ Basis (o. Verbr.): g
+
+
+
+
+
+
+
+
+
Gewicht nach Kategorie
+
+
+
+ $w):
+ ?>
+
+
+ g
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file