Feature: Packlisten-Templates hinzugefügt
Some checks failed
Docker Build & Push / build-and-push (push) Failing after 15s

- 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
This commit is contained in:
Gemini Agent
2025-12-08 21:11:45 +00:00
parent a4b74ab480
commit b9446b01a3
7 changed files with 392 additions and 159 deletions

View File

@@ -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`),

View File

@@ -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 = '<div class="alert alert-danger" role="alert">Fehler: Ein Rucksack kann nicht mehreren Personen zugewiesen werden. Bitte korrigieren Sie die Auswahl.</div>';
} 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 .= '<div class="alert alert-danger" role="alert">SQL Prepare-Fehler: ' . $conn->error . '</div>';
} 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'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich aus Vorlage erstellt!</div>';
} catch (Exception $e) {
// Rollback oder Warnung? Wir lassen die Liste da, aber warnen.
$_SESSION['message'] = '<div class="alert alert-warning" role="alert">Liste erstellt, aber Fehler beim Kopieren der Vorlage: ' . $e->getMessage() . '</div>';
}
} 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'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich erstellt!</div>';
}
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'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich erstellt!</div>';
header("Location: manage_packing_list_items.php?id=" . $new_list_id);
exit;
} else {
@@ -131,6 +166,21 @@ require_once 'header.php';
<label for="name" class="form-label"><i class="fas fa-file-signature me-2 text-muted"></i>Name der Packliste</label>
<input type="text" class="form-control" id="name" value="<?php echo htmlspecialchars($name); ?>" name="name" required>
</div>
<!-- Template Selection -->
<div class="mb-3">
<label for="template_id" class="form-label"><i class="fas fa-copy me-2 text-muted"></i>Vorlage verwenden (Optional)</label>
<select class="form-select" id="template_id" name="template_id">
<option value="0" selected>Keine Vorlage (Leere Liste starten)</option>
<?php foreach ($templates as $tpl): ?>
<option value="<?php echo $tpl['id']; ?>">
<?php echo htmlspecialchars($tpl['name']); ?>
</option>
<?php endforeach; ?>
</select>
<div class="form-text">Wenn du eine Vorlage wählst, werden Artikel, Träger und Rucksäcke daraus übernommen.</div>
</div>
<div class="mb-3">
<label for="description" class="form-label"><i class="fas fa-align-left me-2 text-muted"></i>Beschreibung (optional)</label>
<textarea class="form-control" id="description" name="description" rows="3"><?php echo htmlspecialchars($description); ?></textarea>
@@ -144,43 +194,49 @@ require_once 'header.php';
</div>
<?php endif; ?>
</div>
<div class="col-md-5">
<h5 class="mb-3"><i class="fas fa-users me-2 text-muted"></i>Teilnehmer & Rucksäcke</h5>
<div class="card bg-light border-0">
<div class="card-body">
<p class="small text-muted">Wähle aus, wer mitkommt und wer welchen Rucksack trägt.</p>
<div id="backpack-warning" class="alert alert-warning d-none small p-2 mb-2">
<i class="fas fa-exclamation-triangle me-1"></i> Achtung: Ein Rucksack wurde mehrfach ausgewählt!
</div>
<?php
$user_backpacks_json = [];
$all_assigned_backpack_ids = [];
foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $household_id_for_user);
$current_bp_id = 0;
$user_backpacks_json[$user['id']] = [
'current_id' => $current_bp_id,
'backpacks' => $user_backpacks
];
?>
<div class="mb-3 pb-3 border-bottom">
<div class="form-check mb-2">
<input class="form-check-input participation-check" type="checkbox" name="participate[<?php echo $user['id']; ?>]" value="1" checked id="part_<?php echo $user['id']; ?>">
<label class="form-check-label fw-bold" for="part_<?php echo $user['id']; ?>">
<?php echo htmlspecialchars($user['username']); ?>
</label>
</div>
<div class="ms-4">
<!-- Pass class for JS validation -->
<?php echo render_backpack_card_selector($user, $current_bp_id, $user_backpacks); ?>
</div>
<div id="participants-section">
<div class="card bg-light border-0">
<div class="card-body">
<p class="small text-muted">Wähle aus, wer mitkommt und wer welchen Rucksack trägt.</p>
<div id="backpack-warning" class="alert alert-warning d-none small p-2 mb-2">
<i class="fas fa-exclamation-triangle me-1"></i> Achtung: Ein Rucksack wurde mehrfach ausgewählt!
</div>
<?php endforeach; ?>
<?php
$user_backpacks_json = [];
$all_assigned_backpack_ids = [];
foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $household_id_for_user);
$current_bp_id = 0;
$user_backpacks_json[$user['id']] = [
'current_id' => $current_bp_id,
'backpacks' => $user_backpacks
];
?>
<div class="mb-3 pb-3 border-bottom">
<div class="form-check mb-2">
<input class="form-check-input participation-check" type="checkbox" name="participate[<?php echo $user['id']; ?>]" value="1" checked id="part_<?php echo $user['id']; ?>">
<label class="form-check-label fw-bold" for="part_<?php echo $user['id']; ?>">
<?php echo htmlspecialchars($user['username']); ?>
</label>
</div>
<div class="ms-4">
<!-- Pass class for JS validation -->
<?php echo render_backpack_card_selector($user, $current_bp_id, $user_backpacks); ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div id="template-notice" class="alert alert-info d-none">
<i class="fas fa-info-circle me-2"></i> 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.
</div>
</div>
</div>
@@ -194,25 +250,44 @@ require_once 'header.php';
</div>
</div>
<!-- Custom Script for Validation -->
<!-- Custom Script for Validation & Template Logic -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('createListForm');
const submitBtn = document.getElementById('submitBtn');
const warning = document.getElementById('backpack-warning');
const templateSelect = document.getElementById('template_id');
const participantsSection = document.getElementById('participants-section');
const templateNotice = document.getElementById('template-notice');
// Toggle participants section based on template selection
function toggleParticipants() {
if (templateSelect.value !== "0") {
participantsSection.classList.add('d-none');
templateNotice.classList.remove('d-none');
// Disable validation logic effectively by hiding warning
warning.classList.add('d-none');
submitBtn.disabled = false;
} else {
participantsSection.classList.remove('d-none');
templateNotice.classList.add('d-none');
validateBackpacks(); // Re-validate
}
}
templateSelect.addEventListener('change', toggleParticipants);
// Helper function to check duplicates
function validateBackpacks() {
if (templateSelect.value !== "0") return; // Skip if template is selected
const selected = [];
let hasDuplicate = false;
// Find all hidden inputs that store the backpack ID (usually named backpacks[UserID])
// Since render_backpack_card_selector uses a hidden input with name="backpacks[ID]", we select those
// Find all hidden inputs that store the backpack ID
const inputs = document.querySelectorAll('input[name^="backpacks["]');
inputs.forEach(input => {
// Check if user participates
// The input name is backpacks[123], we need to find checkbox participate[123]
const userIdMatch = input.name.match(/backpacks\[(\d+)\]/);
if (userIdMatch) {
const userId = userIdMatch[1];
@@ -244,15 +319,12 @@ document.addEventListener('DOMContentLoaded', function() {
chk.addEventListener('change', validateBackpacks);
});
// Listen for modal close/hide events (bootstrap specific) to re-validate
// Since the backpack selection happens in a modal, validating on modal hide is efficient.
// We use event delegation or attach to body since modals might be dynamically inserted/removed (though here they are static in footer usually)
document.body.addEventListener('hidden.bs.modal', function (event) {
validateBackpacks();
});
// Initial check
validateBackpacks();
toggleParticipants();
});
</script>

View File

@@ -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'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich zu "'.$copy_name.'" dupliziert!</div>';

View File

@@ -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' : '');
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Details: <?php echo htmlspecialchars($packing_list['name'] ?? ''); ?></h2>
<a href="packing_lists.php" class="btn btn-sm btn-outline-light"><i class="fas fa-arrow-left me-2"></i>Zurück</a>
<a href="<?php echo $back_link; ?>" class="btn btn-sm btn-outline-light"><i class="fas fa-arrow-left me-2"></i>Zurück</a>
</div>
<div class="card-body p-4">
<?php echo $message; ?>

101
src/packing_list_utils.php Normal file
View File

@@ -0,0 +1,101 @@
<?php
// src/packing_list_utils.php - Hilfsfunktionen für Packlisten
/**
* Kopiert den gesamten Inhalt einer Packliste (Items, Träger, Hierarchie) in eine neue Liste.
*
* @param int $sourceId ID der Quell-Packliste
* @param int $targetId ID der Ziel-Packliste
* @param mysqli $conn Datenbankverbindung
* @throws Exception Bei Datenbankfehlern
*/
function copyPackingListContent($sourceId, $targetId, $conn) {
// 1. Träger kopieren (packing_list_carriers)
$stmt_carriers = $conn->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();
}
}
?>

View File

@@ -1,6 +1,5 @@
<?php
// packing_lists.php - Übersicht über alle Haushalts-Packlisten
// KORREKTUR: Bearbeiten-Buttons werden nun auch für Haushaltsmitglieder bei geteilten Listen angezeigt.
// packing_lists.php - Übersicht über alle Haushalts-Packlisten und Vorlagen
$page_title = "Packlisten des Haushalts";
@@ -16,8 +15,10 @@ require_once 'db_connect.php';
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
$view = isset($_GET['view']) && $_GET['view'] === 'templates' ? 'templates' : 'lists';
$is_template_view = ($view === 'templates');
$message = '';
if (isset($_SESSION['message'])) {
$message = $_SESSION['message'];
unset($_SESSION['message']);
@@ -30,25 +31,11 @@ $stmt_household->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 .= '<div class="alert alert-danger" role="alert">SQL Prepare-Fehler: ' . $conn->error . '</div>';
} 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();
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Packlisten im Haushalt</h2>
<a href="add_packing_list.php" class="btn btn-sm btn-outline-light"><i class="fas fa-plus-circle me-2"></i>Neue Packliste erstellen</a>
<div class="card-header">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h4 mb-0"><?php echo $is_template_view ? 'Meine Vorlagen' : 'Packlisten im Haushalt'; ?></h2>
<a href="add_packing_list.php" class="btn btn-sm btn-outline-light"><i class="fas fa-plus-circle me-2"></i>Neue Packliste erstellen</a>
</div>
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link <?php echo !$is_template_view ? 'active' : ''; ?>" href="packing_lists.php?view=lists">
<i class="fas fa-list me-2"></i>Packlisten
</a>
</li>
<li class="nav-item">
<a class="nav-link <?php echo $is_template_view ? 'active' : ''; ?>" href="packing_lists.php?view=templates">
<i class="fas fa-copy me-2"></i>Vorlagen
</a>
</li>
</ul>
</div>
<div class="card-body">
<?php if(!empty($message)) echo $message; ?>
<?php if (empty($packing_lists)): ?>
<div class="alert alert-info text-center" role="alert">
Du hast noch keine Packlisten angelegt. <a href="add_packing_list.php" class="alert-link">Erstelle jetzt deine erste!</a>
<?php if ($is_template_view): ?>
Du hast noch keine Vorlagen. Erstelle eine Packliste und speichere sie als Vorlage.
<?php else: ?>
Du hast noch keine Packlisten angelegt. <a href="add_packing_list.php" class="alert-link">Erstelle jetzt deine erste!</a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="row g-4">
<?php foreach ($packing_lists as $list): ?>
<div class="col-md-6 col-lg-4">
<div class="card h-100 shadow-sm packing-list-card">
<div class="card h-100 shadow-sm packing-list-card <?php echo $is_template_view ? 'border-info' : ''; ?>">
<div class="card-body d-flex flex-column">
<img src="assets/images/rucksack_icon.png" alt="Packliste" class="card-icon">
<div class="d-flex justify-content-between align-items-start mb-2">
<img src="assets/images/rucksack_icon.png" alt="Packliste" class="card-icon" style="opacity: <?php echo $is_template_view ? '0.7' : '1'; ?>;">
<?php if ($is_template_view): ?>
<span class="badge bg-info text-dark">VORLAGE</span>
<?php endif; ?>
</div>
<h5 class="card-title"><?php echo htmlspecialchars($list['name']); ?></h5>
<p class="card-text text-muted flex-grow-1 small"><?php echo htmlspecialchars($list['description'] ?: 'Keine Beschreibung vorhanden.'); ?></p>
<div class="mt-auto d-flex justify-content-between">
<span class="badge bg-secondary creator-badge"><i class="fas fa-user-edit me-1"></i> Erstellt von <?php echo htmlspecialchars($list['creator_name']); ?></span>
<span class="badge bg-secondary creator-badge"><i class="fas fa-user-edit me-1"></i> <?php echo htmlspecialchars($list['creator_name']); ?></span>
<?php if (!empty($list['household_id'])): ?>
<span class="badge bg-success" data-bs-toggle="tooltip" title="Für den Haushalt freigegeben"><i class="fas fa-users"></i></span>
<?php else: ?>
@@ -111,19 +125,50 @@ $conn->close();
<span data-bs-toggle="tooltip" title="Gesamtgewicht"><i class="fas fa-weight-hanging text-muted"></i> <span class="badge bg-success rounded-pill ms-1"><?php echo number_format(($list['total_weight'] ?? 0) / 1000, 2, ',', '.'); ?> kg</span></span>
</div>
<div>
<a href="packing_list_detail.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-success" title="Details ansehen"><i class="fas fa-eye"></i></a>
<?php
$is_owner = ($list['user_id'] == $current_user_id);
$can_edit_household_list = (!empty($list['household_id']) && $list['household_id'] == $current_user_household_id);
// Actions für Vorlagen und Listen unterscheiden
if ($is_template_view) {
// VORLAGEN ACTIONS
?>
<form action="add_packing_list.php" method="POST" class="d-inline">
<!-- Hack: Wir nutzen das Formular von add_packing_list, aber wir müssen es eigentlich via GET -> UI aufrufen oder POST direkt?
add_packing_list erwartet POST mit 'name' etc.
Besser: Link zu add_packing_list.php mit Preset. Aber das haben wir nicht gebaut.
Daher: Nur Edit/Delete hier. Das "Nutzen" passiert im "Erstellen" Dialog.
-->
</form>
<!-- Bearbeiten (Inhalt ändern) -->
<?php if ($is_owner || $can_edit_household_list): ?>
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-primary" title="Vorlage bearbeiten"><i class="fas fa-pencil-alt"></i></a>
<?php endif; ?>
<?php
} else {
// LIST ACTIONS (Standard)
?>
<a href="packing_list_detail.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-success" title="Details ansehen"><i class="fas fa-eye"></i></a>
<?php if ($is_owner || $can_edit_household_list): ?>
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Artikel verwalten"><i class="fas fa-boxes"></i></a>
<?php endif; ?>
<?php if ($is_owner): ?>
<!-- Save As Template -->
<a href="save_as_template.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-warning" title="Als Vorlage speichern"><i class="fas fa-save"></i></a>
<?php endif; ?>
<?php
}
?>
<?php if ($is_owner || $can_edit_household_list): ?>
<a href="edit_packing_list_details.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-primary" title="Details bearbeiten"><i class="fas fa-pencil-alt"></i></a>
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Artikel verwalten"><i class="fas fa-boxes"></i></a>
<?php endif; ?>
<?php if ($is_owner): ?>
<a href="share_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-info" title="Teilen"><i class="fas fa-share-alt"></i></a>
<a href="duplicate_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-dark" title="Duplizieren" onclick="return confirm('Möchten Sie diese Packliste wirklich duplizieren?');"><i class="fas fa-copy"></i></a>
<a href="delete_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-danger ms-2" title="Löschen" onclick="return confirm('Sind Sie sicher, dass Sie diese Packliste löschen möchten?');"><i class="fas fa-trash"></i></a>
<?php if (!$is_template_view): ?>
<a href="duplicate_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-dark" title="Duplizieren" onclick="return confirm('Möchten Sie diese Packliste wirklich duplizieren?');"><i class="fas fa-copy"></i></a>
<?php endif; ?>
<a href="delete_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-danger ms-2" title="Löschen" onclick="return confirm('Sind Sie sicher, dass Sie diese <?php echo $is_template_view ? 'Vorlage' : 'Packliste'; ?> löschen möchten?');"><i class="fas fa-trash"></i></a>
<?php endif; ?>
</div>
</div>
@@ -135,4 +180,4 @@ $conn->close();
</div>
</div>
<?php require_once 'footer.php'; ?>
<?php require_once 'footer.php'; ?>

58
src/save_as_template.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
// src/save_as_template.php - Speichert eine Packliste als Vorlage (Kopie)
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once 'db_connect.php';
require_once 'packing_list_utils.php';
$current_user_id = $_SESSION['user_id'];
$list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if ($list_id <= 0) {
header("Location: packing_lists.php");
exit;
}
// Prüfen, ob Liste existiert und User Zugriff hat
$stmt = $conn->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'] = '<div class="alert alert-danger">Packliste nicht gefunden oder keine Berechtigung.</div>';
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'] = '<div class="alert alert-success">Packliste erfolgreich als Vorlage gespeichert! Du findest sie im Tab "Vorlagen".</div>';
} catch (Exception $e) {
// Aufräumen bei Fehler wäre gut, aber hier einfach Fehler melden
$_SESSION['message'] = '<div class="alert alert-warning">Vorlage erstellt, aber Inhalt konnte nicht vollständig kopiert werden: ' . $e->getMessage() . '</div>';
}
} else {
$_SESSION['message'] = '<div class="alert alert-danger">Fehler beim Erstellen der Vorlage: ' . $conn->error . '</div>';
}
header("Location: packing_lists.php?view=templates");
exit;
?>