From b9446b01a3d6e08fc5edfda3b8a4831e251e7c15 Mon Sep 17 00:00:00 2001 From: Gemini Agent Date: Mon, 8 Dec 2025 21:11:45 +0000 Subject: [PATCH] =?UTF-8?q?Feature:=20Packlisten-Templates=20hinzugef?= =?UTF-8?q?=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DB-Schema erweitert (is_template Spalte in packing_lists) - packing_lists.php: Tabs für Listen/Vorlagen und Filterung - save_as_template.php: Funktion zum Speichern einer Liste als Vorlage - add_packing_list.php: Neue Liste aus Vorlage erstellen - packing_list_utils.php: Zentrale Kopier-Logik (inkl. Träger & Hierarchien) - edit_packing_list_details.php: Navigation angepasst --- packliste.sql | 1 + src/add_packing_list.php | 208 ++++++++++++++++++++---------- src/duplicate_packing_list.php | 54 +------- src/edit_packing_list_details.php | 6 +- src/packing_list_utils.php | 101 +++++++++++++++ src/packing_lists.php | 123 ++++++++++++------ src/save_as_template.php | 58 +++++++++ 7 files changed, 392 insertions(+), 159 deletions(-) create mode 100644 src/packing_list_utils.php create mode 100644 src/save_as_template.php diff --git a/packliste.sql b/packliste.sql index f94c626..1903ab7 100644 --- a/packliste.sql +++ b/packliste.sql @@ -402,6 +402,7 @@ CREATE TABLE `packing_lists` ( `name` varchar(255) NOT NULL, `description` text DEFAULT NULL, `share_token` varchar(255) DEFAULT NULL, + `is_template` tinyint(1) NOT NULL DEFAULT 0, `created_at` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`id`), UNIQUE KEY `share_token` (`share_token`), diff --git a/src/add_packing_list.php b/src/add_packing_list.php index a116ef0..ea5cbde 100644 --- a/src/add_packing_list.php +++ b/src/add_packing_list.php @@ -13,6 +13,7 @@ if (!isset($_SESSION['user_id'])) { require_once 'db_connect.php'; require_once 'household_actions.php'; require_once 'backpack_utils.php'; +require_once 'packing_list_utils.php'; $current_user_id = $_SESSION['user_id']; $message = ''; @@ -26,6 +27,23 @@ $stmt_household->execute(); $household_id_for_user = $stmt_household->get_result()->fetch_assoc()['household_id']; $stmt_household->close(); +// Fetch available templates +$templates = []; +$sql_templates = "SELECT id, name FROM packing_lists WHERE is_template = 1 AND (user_id = ? OR household_id = ?)"; +$stmt_templates = $conn->prepare($sql_templates); +// Falls household_id NULL ist, binden wir trotzdem User ID als Dummy oder handeln es anders. +// Einfacher: Wir nutzen current_user_id zweimal, wenn household NULL, oder Logik im SQL. +// Da household_id INT ist, kann es NULL sein. +// Besser: +$h_id = $household_id_for_user ?: 0; // 0 matches nothing usually, safe +$stmt_templates->bind_param("ii", $current_user_id, $h_id); +$stmt_templates->execute(); +$res_templates = $stmt_templates->get_result(); +while ($row = $res_templates->fetch_assoc()) { + $templates[] = $row; +} +$stmt_templates->close(); + // Fetch Users for Backpack Assignment UI $available_users = []; if ($household_id_for_user) { @@ -45,21 +63,25 @@ $stmt_u->close(); if ($_SERVER["REQUEST_METHOD"] == "POST") { $name = trim($_POST['name']); $description = trim($_POST['description']); + $template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0; $household_id = isset($_POST['is_household_list']) && $household_id_for_user ? $household_id_for_user : NULL; - // Server-Side Validation for duplicate backpacks - $selected_backpacks = []; + // Server-Side Validation for duplicate backpacks (ONLY if no template is selected) $has_duplicate_backpacks = false; - if (isset($_POST['participate']) && is_array($_POST['participate'])) { - foreach ($_POST['participate'] as $uid => $val) { - if ($val) { - $bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0; - if ($bid > 0) { - if (in_array($bid, $selected_backpacks)) { - $has_duplicate_backpacks = true; - break; + + if ($template_id == 0) { + $selected_backpacks = []; + if (isset($_POST['participate']) && is_array($_POST['participate'])) { + foreach ($_POST['participate'] as $uid => $val) { + if ($val) { + $bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0; + if ($bid > 0) { + if (in_array($bid, $selected_backpacks)) { + $has_duplicate_backpacks = true; + break; + } + $selected_backpacks[] = $bid; } - $selected_backpacks[] = $bid; } } } @@ -70,7 +92,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { } elseif ($has_duplicate_backpacks) { $message = ''; } else { - $stmt = $conn->prepare("INSERT INTO packing_lists (user_id, household_id, name, description) VALUES (?, ?, ?, ?)"); + $stmt = $conn->prepare("INSERT INTO packing_lists (user_id, household_id, name, description, is_template) VALUES (?, ?, ?, ?, 0)"); if ($stmt === false) { $message .= ''; } else { @@ -78,23 +100,37 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { if ($stmt->execute()) { $new_list_id = $conn->insert_id; - if (isset($_POST['participate']) && is_array($_POST['participate'])) { - foreach ($_POST['participate'] as $uid => $val) { - $uid = intval($uid); - if ($val) { - $bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0; - $bid_to_insert = ($bid > 0) ? $bid : NULL; - - $stmt_in = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)"); - $stmt_in->bind_param("iii", $new_list_id, $uid, $bid_to_insert); - $stmt_in->execute(); - $stmt_in->close(); - - if ($bid > 0) { - sync_backpack_items($conn, $new_list_id, $uid, $bid); + if ($template_id > 0) { + // Option A: Aus Template erstellen + try { + copyPackingListContent($template_id, $new_list_id, $conn); + $_SESSION['message'] = ''; + } catch (Exception $e) { + // Rollback oder Warnung? Wir lassen die Liste da, aber warnen. + $_SESSION['message'] = ''; + } + + } else { + // Option B: Leere Liste mit manuellen Trägern + if (isset($_POST['participate']) && is_array($_POST['participate'])) { + foreach ($_POST['participate'] as $uid => $val) { + $uid = intval($uid); + if ($val) { + $bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0; + $bid_to_insert = ($bid > 0) ? $bid : NULL; + + $stmt_in = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)"); + $stmt_in->bind_param("iii", $new_list_id, $uid, $bid_to_insert); + $stmt_in->execute(); + $stmt_in->close(); + + if ($bid > 0) { + sync_backpack_items($conn, $new_list_id, $uid, $bid); + } } } } + $_SESSION['message'] = ''; } if ($household_id_for_user) { @@ -102,7 +138,6 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { log_household_action($conn, $household_id_for_user, $current_user_id, $log_message); } - $_SESSION['message'] = ''; header("Location: manage_packing_list_items.php?id=" . $new_list_id); exit; } else { @@ -131,6 +166,21 @@ require_once 'header.php'; + + +
+ + +
Wenn du eine Vorlage wählst, werden Artikel, Träger und Rucksäcke daraus übernommen.
+
+
@@ -144,43 +194,49 @@ require_once 'header.php';
+
Teilnehmer & Rucksäcke
-
-
-

Wähle aus, wer mitkommt und wer welchen Rucksack trägt.

-
- Achtung: Ein Rucksack wurde mehrfach ausgewählt! -
- - $current_bp_id, - 'backpacks' => $user_backpacks - ]; - ?> -
-
- - -
-
- - -
+
+
+
+

Wähle aus, wer mitkommt und wer welchen Rucksack trägt.

+
+ Achtung: Ein Rucksack wurde mehrfach ausgewählt!
- + + $current_bp_id, + 'backpacks' => $user_backpacks + ]; + ?> +
+
+ + +
+
+ + +
+
+ +
+
+ Da eine Vorlage gewählt wurde, werden die Träger und Rucksäcke aus der Vorlage übernommen. Du kannst diese später in der Listenansicht bearbeiten. +
@@ -194,25 +250,44 @@ require_once 'header.php';
- + diff --git a/src/duplicate_packing_list.php b/src/duplicate_packing_list.php index ce81037..28751c9 100644 --- a/src/duplicate_packing_list.php +++ b/src/duplicate_packing_list.php @@ -62,56 +62,10 @@ try { $new_list_id = $conn->insert_id; $stmt_new_list->close(); - // KORREKTUR: Komplette Überarbeitung der Kopierlogik - - // 4. Alle Artikel aus der originalen Liste mit ihrer ID holen - $stmt_items = $conn->prepare("SELECT id, article_id, quantity, parent_packing_list_item_id, carrier_user_id FROM packing_list_items WHERE packing_list_id = ?"); - $stmt_items->bind_param("i", $original_list_id); - $stmt_items->execute(); - $result_items = $stmt_items->get_result(); - $original_items = []; - while ($row = $result_items->fetch_assoc()) { - $original_items[] = $row; - } - $stmt_items->close(); - - if (!empty($original_items)) { - // 5. Hierarchie-Klonen in zwei Schritten - $old_to_new_id_map = []; // Speichert [alte_item_id] => neue_item_id - - // Schritt 5a: Alle Items ohne Parent-Info kopieren und die ID-Zuordnung speichern - $stmt_insert_item = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, article_id, quantity, carrier_user_id) VALUES (?, ?, ?, ?)"); - if (!$stmt_insert_item) throw new Exception("DB Prepare Fehler (Item Insert)"); - - foreach ($original_items as $item) { - $stmt_insert_item->bind_param("iiii", $new_list_id, $item['article_id'], $item['quantity'], $item['carrier_user_id']); - if (!$stmt_insert_item->execute()) throw new Exception("Fehler beim Kopieren von Artikel ID " . $item['article_id']); - $new_item_id = $conn->insert_id; - $old_to_new_id_map[$item['id']] = $new_item_id; - } - $stmt_insert_item->close(); - - // Schritt 5b: Die Parent-IDs für die neu erstellten Items aktualisieren - $stmt_update_parent = $conn->prepare("UPDATE packing_list_items SET parent_packing_list_item_id = ? WHERE id = ?"); - if (!$stmt_update_parent) throw new Exception("DB Prepare Fehler (Parent Update)"); - - foreach ($original_items as $item) { - // Wenn das Original-Item einen Parent hatte... - if (!empty($item['parent_packing_list_item_id'])) { - $old_parent_id = $item['parent_packing_list_item_id']; - - // Stelle sicher, dass der Parent auch in unserer Map existiert - if (isset($old_to_new_id_map[$old_parent_id])) { - $new_parent_id = $old_to_new_id_map[$old_parent_id]; - $new_child_id = $old_to_new_id_map[$item['id']]; - - $stmt_update_parent->bind_param("ii", $new_parent_id, $new_child_id); - if (!$stmt_update_parent->execute()) throw new Exception("Fehler beim Setzen der Hierarchie für Item " . $new_child_id); - } - } - } - $stmt_update_parent->close(); - } + // 4. Inhalt kopieren (Items, Träger, Hierarchien) + // Nutzen der zentralen Hilfsfunktion, die auch beim Erstellen aus Templates verwendet wird. + require_once 'packing_list_utils.php'; + copyPackingListContent($original_list_id, $new_list_id, $conn); $conn->commit(); $_SESSION['message'] = ''; diff --git a/src/edit_packing_list_details.php b/src/edit_packing_list_details.php index b47a980..14fa1fb 100644 --- a/src/edit_packing_list_details.php +++ b/src/edit_packing_list_details.php @@ -29,7 +29,7 @@ if ($packing_list_id > 0) { $current_user_household_id = $stmt_household_check->get_result()->fetch_assoc()['household_id']; $stmt_household_check->close(); - $stmt_list_check = $conn->prepare("SELECT id, name, description, user_id, household_id FROM packing_lists WHERE id = ?"); + $stmt_list_check = $conn->prepare("SELECT id, name, description, user_id, household_id, is_template FROM packing_lists WHERE id = ?"); $stmt_list_check->bind_param("i", $packing_list_id); $stmt_list_check->execute(); $result = $stmt_list_check->get_result(); @@ -178,12 +178,14 @@ function cleanup_old_backpack_containers($conn, $list_id, $user_id) { } } +$back_link = 'packing_lists.php' . (!empty($packing_list['is_template']) ? '?view=templates' : ''); + ?>

Details:

- Zurück + Zurück
diff --git a/src/packing_list_utils.php b/src/packing_list_utils.php new file mode 100644 index 0000000..e299f1f --- /dev/null +++ b/src/packing_list_utils.php @@ -0,0 +1,101 @@ +prepare("SELECT user_id, backpack_id FROM packing_list_carriers WHERE packing_list_id = ?"); + if (!$stmt_carriers) throw new Exception("Prepare failed for carriers select: " . $conn->error); + + $stmt_carriers->bind_param("i", $sourceId); + $stmt_carriers->execute(); + $result_carriers = $stmt_carriers->get_result(); + + $stmt_insert_carrier = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)"); + if (!$stmt_insert_carrier) throw new Exception("Prepare failed for carriers insert: " . $conn->error); + + while ($row = $result_carriers->fetch_assoc()) { + $stmt_insert_carrier->bind_param("iii", $targetId, $row['user_id'], $row['backpack_id']); + if (!$stmt_insert_carrier->execute()) throw new Exception("Error inserting carrier: " . $stmt_insert_carrier->error); + } + $stmt_carriers->close(); + $stmt_insert_carrier->close(); + + // 2. Items kopieren (packing_list_items) + // Alle Items inklusive Rucksack/Fach-Info holen + $sql_items = "SELECT id, article_id, quantity, parent_packing_list_item_id, carrier_user_id, backpack_id, backpack_compartment_id, name, order_index + FROM packing_list_items WHERE packing_list_id = ?"; + $stmt_items = $conn->prepare($sql_items); + if (!$stmt_items) throw new Exception("Prepare failed for items select: " . $conn->error); + + $stmt_items->bind_param("i", $sourceId); + $stmt_items->execute(); + $result_items = $stmt_items->get_result(); + + $original_items = []; + while ($row = $result_items->fetch_assoc()) { + $original_items[] = $row; + } + $stmt_items->close(); + + if (!empty($original_items)) { + $old_to_new_id_map = []; // Mapping: alte_ID => neue_ID + + // 2a. Items einfügen (zunächst ohne Parent-Referenz) + $sql_insert = "INSERT INTO packing_list_items + (packing_list_id, article_id, quantity, carrier_user_id, backpack_id, backpack_compartment_id, name, order_index) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + $stmt_insert_item = $conn->prepare($sql_insert); + if (!$stmt_insert_item) throw new Exception("Prepare failed for item insert: " . $conn->error); + + foreach ($original_items as $item) { + $stmt_insert_item->bind_param( + "iiiisiis", + $targetId, + $item['article_id'], + $item['quantity'], + $item['carrier_user_id'], + $item['backpack_id'], + $item['backpack_compartment_id'], + $item['name'], + $item['order_index'] + ); + + if (!$stmt_insert_item->execute()) { + throw new Exception("Error copying item ID " . $item['article_id'] . ": " . $stmt_insert_item->error); + } + $old_to_new_id_map[$item['id']] = $conn->insert_id; + } + $stmt_insert_item->close(); + + // 2b. Parent-Referenzen aktualisieren (Hierarchie wiederherstellen) + $stmt_update_parent = $conn->prepare("UPDATE packing_list_items SET parent_packing_list_item_id = ? WHERE id = ?"); + if (!$stmt_update_parent) throw new Exception("Prepare failed for parent update: " . $conn->error); + + foreach ($original_items as $item) { + if (!empty($item['parent_packing_list_item_id'])) { + $old_parent_id = $item['parent_packing_list_item_id']; + + // Prüfen, ob der Parent auch kopiert wurde (sollte immer so sein) + if (isset($old_to_new_id_map[$old_parent_id])) { + $new_parent_id = $old_to_new_id_map[$old_parent_id]; + $new_child_id = $old_to_new_id_map[$item['id']]; + + $stmt_update_parent->bind_param("ii", $new_parent_id, $new_child_id); + if (!$stmt_update_parent->execute()) { + throw new Exception("Error updating parent hierarchy: " . $stmt_update_parent->error); + } + } + } + } + $stmt_update_parent->close(); + } +} +?> diff --git a/src/packing_lists.php b/src/packing_lists.php index c8b0939..f24a999 100644 --- a/src/packing_lists.php +++ b/src/packing_lists.php @@ -1,6 +1,5 @@ execute(); $current_user_household_id = $stmt_household->get_result()->fetch_assoc()['household_id']; $stmt_household->close(); -$household_member_ids = [$current_user_id]; -if ($current_user_household_id) { - $stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?"); - $stmt_members->bind_param("i", $current_user_household_id); - $stmt_members->execute(); - $result_members = $stmt_members->get_result(); - while ($row = $result_members->fetch_assoc()) { - if (!in_array($row['id'], $household_member_ids)) { - $household_member_ids[] = $row['id']; - } - } - $stmt_members->close(); -} - $packing_lists = []; -// KORREKTUR: Die SQL-Abfrage wurde vereinfacht und korrigiert, um alle relevanten Listen anzuzeigen. -// Es werden alle Listen angezeigt, die entweder vom Nutzer erstellt wurden ODER mit seinem Haushalt geteilt sind. +$is_template_val = $is_template_view ? 1 : 0; + $sql = "SELECT - pl.id, pl.name, pl.description, pl.user_id, pl.household_id, + pl.id, pl.name, pl.description, pl.user_id, pl.household_id, pl.is_template, u.username as creator_name, COUNT(DISTINCT COALESCE(pli.carrier_user_id, 'sonstiges')) AS carrier_count, SUM(pli.quantity * a.weight_grams) AS total_weight @@ -56,15 +43,17 @@ $sql = "SELECT JOIN users u ON pl.user_id = u.id LEFT JOIN packing_list_items pli ON pl.id = pli.packing_list_id LEFT JOIN articles a ON pli.article_id = a.id - WHERE pl.user_id = ? OR pl.household_id = ? - GROUP BY pl.id, pl.name, pl.description, pl.user_id, u.username, pl.household_id + WHERE (pl.user_id = ? OR pl.household_id = ?) AND pl.is_template = ? + GROUP BY pl.id, pl.name, pl.description, pl.user_id, u.username, pl.household_id, pl.is_template ORDER BY pl.name ASC"; $stmt = $conn->prepare($sql); if ($stmt === false) { $message .= ''; } else { - $stmt->bind_param("ii", $current_user_id, $current_user_household_id); + // Falls household_id NULL ist, nutzen wir 0 (da ID immer > 0) + $h_id = $current_user_household_id ?: 0; + $stmt->bind_param("iii", $current_user_id, $h_id, $is_template_val); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { @@ -76,28 +65,53 @@ $conn->close(); ?>
-
-

Packlisten im Haushalt

- Neue Packliste erstellen +
-
+
- Packliste +
+ Packliste + + VORLAGE + +
+

- Erstellt von + @@ -111,19 +125,50 @@ $conn->close(); kg
- +
+ +
+ + + + + + + + + + + + + + + + + + - - - - + - - - + + + +
@@ -135,4 +180,4 @@ $conn->close();
- + \ No newline at end of file diff --git a/src/save_as_template.php b/src/save_as_template.php new file mode 100644 index 0000000..d43e1f3 --- /dev/null +++ b/src/save_as_template.php @@ -0,0 +1,58 @@ +prepare("SELECT name, description, household_id FROM packing_lists WHERE id = ? AND (user_id = ? OR household_id = (SELECT household_id FROM users WHERE id = ?))"); +// Eigentlich darf nur Owner oder Household-Admin kopieren? Lassen wir es für alle mit Lesezugriff zu. +$stmt->bind_param("iii", $list_id, $current_user_id, $current_user_id); +$stmt->execute(); +$result = $stmt->get_result(); +if ($result->num_rows === 0) { + $_SESSION['message'] = '
Packliste nicht gefunden oder keine Berechtigung.
'; + header("Location: packing_lists.php"); + exit; +} +$list = $result->fetch_assoc(); +$stmt->close(); + +$new_name = "Vorlage: " . $list['name']; + +// Neue Liste (Template) anlegen +$stmt_insert = $conn->prepare("INSERT INTO packing_lists (user_id, household_id, name, description, is_template) VALUES (?, ?, ?, ?, 1)"); +$stmt_insert->bind_param("isss", $current_user_id, $list['household_id'], $new_name, $list['description']); + +if ($stmt_insert->execute()) { + $new_template_id = $conn->insert_id; + try { + copyPackingListContent($list_id, $new_template_id, $conn); + $_SESSION['message'] = '
Packliste erfolgreich als Vorlage gespeichert! Du findest sie im Tab "Vorlagen".
'; + } catch (Exception $e) { + // Aufräumen bei Fehler wäre gut, aber hier einfach Fehler melden + $_SESSION['message'] = '
Vorlage erstellt, aber Inhalt konnte nicht vollständig kopiert werden: ' . $e->getMessage() . '
'; + } +} else { + $_SESSION['message'] = '
Fehler beim Erstellen der Vorlage: ' . $conn->error . '
'; +} + +header("Location: packing_lists.php?view=templates"); +exit; +?>