Compare commits

..

25 Commits

Author SHA1 Message Date
Gemini Agent
7744168d0d Fix: Restore 'Edit Details' button for standard packing lists
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 23s
2025-12-08 21:44:43 +00:00
Gemini Agent
7bdc421d90 Fix: Correct display of new templates and limit carriers to assigned users
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 19s
2025-12-08 21:40:13 +00:00
Gemini Agent
0753e2c4b9 Fix: Template improvements
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 38s
- api_packing_list_handler.php: Use robust LEFT JOINs to fetch backpack/compartment names, fixing '0' display issue for templates.
- packing_lists.php: Add 'Edit Details' button for templates.
- edit_packing_list_details.php: Allow sharing templates with household (checkbox) and adjust title for templates.
2025-12-08 21:35:08 +00:00
Gemini Agent
a19cfa98f0 Fix: Add Auto-Migration for 'is_template' column in packing_lists.php to prevent SQL errors on deployment
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 17s
2025-12-08 21:28:34 +00:00
8c50910363 .gitea/workflows/build-push.yaml aktualisiert
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 25s
2025-12-08 21:20:01 +00:00
Gemini Agent
59104fb3bd Docs: Update README with Template feature details
Some checks failed
Docker Build & Push / build-and-push (push) Failing after 4s
2025-12-08 21:12:08 +00:00
Gemini Agent
b9446b01a3 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
2025-12-08 21:11:45 +00:00
Gemini Agent
a4b74ab480 Docs & Fix: packliste.sql aktualisiert, Auto-Migration und Changelog ergänzt.
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 14s
2025-12-07 08:06:24 +00:00
Gemini Agent
991f264e29 Fix: Rucksack-Kategorie (DB/UI/Stats) und Tooltip-Zuverlässigkeit
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 28s
2025-12-07 08:04:32 +00:00
Gemini Agent
80ede8e3a6 Fix: Zusatztaschen-Gewicht und Bilder in Packlisten
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 16s
- backpack_utils.php: Setze article_id beim Sync von Zusatztaschen korrekt.
- packing_list_detail.php: Zeige Artikelbild statt Ordner-Icon bei Zusatztaschen.
2025-12-07 07:57:05 +00:00
Gemini Agent
70eef4cd82 Fix: Gewichtsberechnung Zusatztaschen, UI-Konsistenz bei Editieren, Icons
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 23s
2025-12-07 07:52:03 +00:00
Gemini Agent
967bae965a Fix: TomSelect Dropdown-Anzeige (Z-Index, Images) in Rucksack-Bearbeitung
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 16s
2025-12-07 07:46:17 +00:00
Gemini Agent
f9df3087a7 Fix: Auto-Migration in backpacks.php, um SQL-Fehler bei fehlenden Spalten zu verhindern.
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 20s
2025-12-07 07:40:07 +00:00
Gemini Agent
dd1cbcb2a6 Feature: Modulare Rucksäcke (Zusatztaschen)
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 26s
- DB-Erweiterung für linked_article_id in backpack_compartments.
- UI in edit_backpack.php zum Verknüpfen von Artikeln als Fächer.
- Sync-Logik in backpack_utils.php übernimmt Artikel-Daten.
- Gewichts-Kalkulation in backpacks.php berücksichtigt Zusatztaschen.
- Changelog aktualisiert.
2025-12-07 07:36:34 +00:00
Gemini Agent
f1d9634dba Merge branch 'master' of https://git.klenzel.net/admin/packliste
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 22s
2025-12-06 17:28:12 +00:00
Gemini Agent
36660fdd51 Feature: Bildbibliothek als Modal in Artikel-Erstellung (bis zu 500 Bilder) 2025-12-06 17:28:07 +00:00
0c4cc705a6 .gitea/workflows/build-push.yaml aktualisiert
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 17s
2025-12-06 17:23:13 +00:00
df2a98ae28 .gitea/workflows/build-push.yaml aktualisiert
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 16s
2025-12-06 17:01:34 +00:00
Gemini Agent
f7733dfce5 Fix: Chart colors forced to green palette (no blue tones)
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 24s
2025-12-06 16:50:56 +00:00
Gemini Agent
620c4127c5 Fix: Validierung, Charts & Design
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 20s
- backpack_utils.php: JS-Logik korrigiert, damit Rucksäcke dynamisch gesperrt werden.
- add_packing_list.php: Performance-Fix und Validierung.
- packing_list_detail.php: Chart-Farben verbessert, Padding hinzugefügt.
- backpacks.php: Button-Farbe.
2025-12-06 16:45:48 +00:00
Gemini Agent
c1f4ec1de7 Fix: UI/UX Verbesserungen und Critical Fix
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 23s
- add_packing_list.php: Critical Fix für Browser-Freeze (MutationObserver ersetzt).
- packing_list_detail.php: Chart-Design optimiert (Kontrast, Rahmen entfernt).
- backpacks.php: Button-Farbe angepasst.
- README.md: Changelog aktualisiert.
2025-12-06 16:40:58 +00:00
Gemini Agent
1c13862640 Docs: Changelog für 06.12.2025 hinzugefügt
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 12s
2025-12-06 16:35:01 +00:00
Gemini Agent
aaf31a3b4b Feature: Rucksack-Link, Packlisten-Validierung und Detail-Statistiken
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 28s
- backpacks.php: Button für Hersteller-Link hinzugefügt.
- add_packing_list.php: Validierung gegen doppelte Rucksack-Zuweisung (JS & PHP).
- packing_list_detail.php: Charts auf Grüntöne umgestellt, Modal für Träger-Statistiken hinzugefügt.
2025-12-06 16:33:56 +00:00
Gemini Agent
47e51ce60f Merge branch 'master' of https://git.klenzel.net/admin/packliste
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 12s
2025-12-06 16:24:16 +00:00
Gemini Agent
0392c69019 Feature: Verbesserungen an Artikel-Bildern, Rucksäcken und Filtern
- add_article.php: Auswahl bereits hochgeladener Bilder hinzugefügt.
- edit_backpack.php: Link zur Herstellerseite (neue Spalte product_url) und Fix für 'Neuer Hersteller' hinzugefügt. Auto-Migration für DB-Spalte integriert.
- manage_packing_list_items.php: Filteroptionen um 'Alle' erweitert.
2025-12-06 16:20:15 +00:00
16 changed files with 1463 additions and 532 deletions

View File

@@ -6,7 +6,7 @@ on:
branches:
- main
- master
jobs:
build-and-push:
runs-on: ubuntu-latest
@@ -35,4 +35,24 @@ jobs:
echo "Baue Image: $IMAGE_TAG"
docker build -t $IMAGE_TAG .
docker push $IMAGE_TAG
docker push $IMAGE_TAG
- name: Webhook an Node-RED
if: always()
run: |
# Status setzen
if [ "${{ job.status }}" == "success" ]; then
STATUS="success"
else
STATUS="failed"
fi
# JSON Payload basteln
# Wir nutzen printf, um sauberes JSON zu bauen
JSON_DATA=$(printf '{"status": "%s", "repo": "%s", "actor": "%s"}' "$STATUS" "${{ gitea.repository }}" "${{ gitea.actor }}")
# Abfeuern an Node-RED
curl -v -H "Content-Type: application/json" \
-X POST \
-d "$JSON_DATA" \
http://172.30.80.246:1880/gitea-status

View File

@@ -14,6 +14,7 @@ Eine moderne, webbasierte Anwendung zur Verwaltung von Packlisten für Wanderung
* [Drucken & Export](#5-drucken--export)
* [Profil & Einstellungen](#6-profil--einstellungen)
4. [Technologie](#technologie)
5. [Changelog](#changelog)
---
@@ -126,7 +127,16 @@ Dies ist das Herzstück der Anwendung.
1. Gehe zu **"Packlisten"** und erstelle eine neue Liste.
2. **Rucksack-Zuweisung:** Wähle direkt beim Erstellen (oder später unter "Details bearbeiten"), wer welchen Rucksack trägt.
3. Klicke in der Übersicht auf **"Artikel verwalten"** (das Box-Icon).
3. **Vorlagen nutzen:** Wähle optional eine Vorlage aus, um deine Liste mit vordefinierten Artikeln und Strukturen zu starten.
4. Klicke in der Übersicht auf **"Artikel verwalten"** (das Box-Icon).
#### Packlisten-Vorlagen (Templates)
Du kannst jede existierende Packliste als Vorlage speichern, um sie später wiederzuverwenden.
* **Vorlage erstellen:** Klicke in der Übersicht "Packlisten" bei einer deiner Listen auf das gelbe "Speichern"-Icon ("Als Vorlage speichern").
* **Vorlagen verwalten:** Wechsle in der Übersicht oben auf den Tab **"Vorlagen"**. Hier kannst du deine Vorlagen bearbeiten oder löschen.
* **Liste aus Vorlage:** Beim Erstellen einer neuen Packliste kannst du im Dropdown "Vorlage verwenden" eine deiner gespeicherten Vorlagen auswählen. Der gesamte Inhalt (Artikel, Fächer, Träger) wird in die neue Liste kopiert.
#### Der Packlisten-Editor (Drag & Drop)
@@ -158,4 +168,41 @@ Das Projekt basiert auf bewährten Web-Standards:
* **Backend:** PHP 8.2 (Natives PHP, keine schweren Frameworks)
* **Frontend:** HTML5, CSS3 (Custom Theme), Bootstrap 5
* **Datenbank:** MariaDB / MySQL
* **JavaScript:** Vanilla JS + Sortable.js (für Drag & Drop) + Chart.js (für Diagramme).
* **JavaScript:** Vanilla JS + Sortable.js (für Drag & Drop) + Chart.js (für Diagramme).
---
## Changelog
### 08.12.2025
* **Feature: Packlisten-Templates**
* Neue Funktion zum Speichern von Packlisten als Vorlagen.
* Separater Tab "Vorlagen" in der Übersicht.
* Erstellen von neuen Listen auf Basis von Vorlagen (kopiert Artikel, Hierarchie und Trägerzuordnung).
* Zentrale Kopier-Logik implementiert (Fix für `duplicate_packing_list.php`: Rucksäcke werden nun korrekt mitkopiert).
### 06.12.2025
* **Rucksäcke:**
* Neues Feld für Hersteller-Link (Product URL) hinzugefügt.
* "Info"-Button in der Übersicht öffnet den hinterlegten Link (Design angepasst auf Grün).
* Fix: Eingabefeld für neue Hersteller erscheint nun zuverlässig.
* **Artikel:**
* Komfort-Funktion: Auswahl aus den letzten 24 hochgeladenen Bildern beim Erstellen neuer Artikel hinzugefügt.
* **Packlisten:**
* **Editor:** Filter-Optionen ("Alle Kategorien", "Alle Hersteller") korrigiert und erweitert.
* **Erstellung:**
* Validierung hinzugefügt, die verhindert, dass ein Rucksack mehrfach zugewiesen wird.
* **Critical Fix:** Browser-Freeze durch Endlosschleife bei der Validierung behoben.
* **Details:**
* Design-Update: Diagramme verwenden nun kontrastreiche Grüntöne passend zum Thema (ohne weiße Rahmen).
* Statistik: Klickbare Trägernamen öffnen ein Modal mit detaillierten Gewichtsstatistiken pro Kategorie.
* **Modulare Rucksack-Erweiterung:**
* Fächer können nun direkt mit Artikeln verknüpft werden (z.B. "Hüftgurttasche").
* Diese "Zusatztaschen" werden in der Packliste automatisch als Container angelegt.
* Das Gewicht der Zusatztaschen wird automatisch zum Rucksack-Gesamtgewicht addiert.
* **Rucksack-Kategorie:** Rucksäcke können nun einer Kategorie (z.B. "Transport") zugewiesen werden, damit sie in der Statistik korrekt auftauchen.
* **Fixes:**
* Datenbank-Selbstheilung (`Auto-Migration`) in `backpacks.php` und `edit_backpack.php` integriert.
* `packliste.sql` aktualisiert.
* Anzeige-Probleme bei Dropdowns behoben.
* SQL-Fehler in der Statistik-Berechnung korrigiert.

View File

@@ -75,9 +75,12 @@ CREATE TABLE `backpack_compartments` (
`backpack_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`sort_order` int(11) DEFAULT 0,
`linked_article_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `backpack_id` (`backpack_id`),
CONSTRAINT `backpack_compartments_ibfk_1` FOREIGN KEY (`backpack_id`) REFERENCES `backpacks` (`id`) ON DELETE CASCADE
KEY `fk_bc_article` (`linked_article_id`),
CONSTRAINT `backpack_compartments_ibfk_1` FOREIGN KEY (`backpack_id`) REFERENCES `backpacks` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_bc_article` FOREIGN KEY (`linked_article_id`) REFERENCES `articles` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
@@ -108,10 +111,14 @@ CREATE TABLE `backpacks` (
`weight_grams` int(11) DEFAULT 0,
`volume_liters` int(11) DEFAULT 0,
`image_url` varchar(255) DEFAULT NULL,
`product_url` varchar(255) DEFAULT NULL,
`category_id` int(11) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `backpacks_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
KEY `fk_bp_category` (`category_id`),
CONSTRAINT `backpacks_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_bp_category` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
@@ -395,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

@@ -1,7 +1,6 @@
<?php
// add_article.php - Formular zum Hinzufügen eines neuen Artikels
// FINALE, VOLLSTÄNDIGE VERSION mit allen Funktionen (Komponenten, Bestand, etc.)
// KORREKTUR: SSL-Verifizierung und @-Fehlerunterdrückung korrigiert.
// FINALE, VOLLSTÄNDIGE VERSION mit Bilder-Bibliothek Modal
$page_title = "Neuen Artikel hinzufügen";
@@ -45,25 +44,28 @@ if ($household_id_for_user) {
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
// Lade Kategorien, Hersteller, Lagerorte und potenzielle Eltern-Artikel
// Lade Kategorien
$stmt_cat_load = $conn->prepare("SELECT id, name FROM categories WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_cat_load->bind_param($types, ...$household_member_ids);
$stmt_cat_load->execute();
$categories = $stmt_cat_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_cat_load->close();
// Lade Hersteller
$stmt_man_load = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_man_load->bind_param($types, ...$household_member_ids);
$stmt_man_load->execute();
$manufacturers = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_man_load->close();
// Lade Lagerorte
$stmt_loc_load = $conn->prepare("SELECT id, name, parent_id FROM storage_locations WHERE user_id IN ($placeholders) ORDER BY parent_id, name");
$stmt_loc_load->bind_param($types, ...$household_member_ids);
$stmt_loc_load->execute();
$all_locations = $stmt_loc_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_loc_load->close();
// Lade Eltern-Artikel
$all_parent_params = array_merge($household_member_ids, [$household_id_for_user]);
$all_parent_types = $types . 'i';
$stmt_parent_articles = $conn->prepare("SELECT a.id, a.name, m.name as manufacturer_name, a.product_designation FROM articles a LEFT JOIN manufacturers m ON a.manufacturer_id = m.id WHERE (a.user_id IN ($placeholders) OR a.household_id = ?) AND a.parent_article_id IS NULL ORDER BY a.name ASC");
@@ -72,6 +74,14 @@ $stmt_parent_articles->execute();
$parent_articles = $stmt_parent_articles->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_parent_articles->close();
// Lade ALLE Bilder (für Auswahl im Modal) - Limit auf 500 für Performance, aber scrollbar
$stmt_imgs = $conn->prepare("SELECT DISTINCT image_url FROM articles WHERE user_id IN ($placeholders) AND image_url IS NOT NULL AND image_url != '' AND image_url != '0' ORDER BY created_at DESC LIMIT 500");
$stmt_imgs->bind_param($types, ...$household_member_ids);
$stmt_imgs->execute();
$all_images = $stmt_imgs->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_imgs->close();
// Strukturierung Lagerorte
$storage_locations_structured = [];
foreach ($all_locations as $loc) { if ($loc['parent_id'] === NULL) { if (!isset($storage_locations_structured[$loc['id']])) { $storage_locations_structured[$loc['id']] = ['name' => $loc['name'], 'children' => []]; } } }
foreach ($all_locations as $loc) { if ($loc['parent_id'] !== NULL && isset($storage_locations_structured[$loc['parent_id']])) { $storage_locations_structured[$loc['parent_id']]['children'][] = $loc; } }
@@ -80,50 +90,31 @@ $upload_dir = 'uploads/images/';
if (!is_dir($upload_dir)) { @mkdir($upload_dir, 0777, true); }
function save_image_from_url($url, $upload_dir) {
// KORREKTUR: Unsichere SSL-Optionen entfernt, @-Operator entfernt
$context_options = ["http" => ["header" => "User-Agent: Mozilla/5.0\r\n", "timeout" => 10]];
$context = stream_context_create($context_options);
// Fehlerbehandlung für file_get_contents
$image_data = file_get_contents($url, false, $context);
if ($image_data === false) {
return [false, "Bild konnte von der URL nicht heruntergeladen werden. Überprüfen Sie den Link und die Erreichbarkeit."];
}
// Fehlerbehandlung für getimagesizefromstring
$image_info = getimagesizefromstring($image_data);
if ($image_info === false) {
return [false, "Die angegebene URL führt nicht zu einem gültigen Bild."];
}
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($image_info['mime'], $allowed_mime_types)) {
return [false, "Nicht unterstützter Bildtyp: " . htmlspecialchars($image_info['mime'])];
}
$extension = image_type_to_extension($image_info[2], false);
$unique_file_name = uniqid('img_url_', true) . '.' . $extension;
$destination = $upload_dir . $unique_file_name;
if (file_put_contents($destination, $image_data)) {
return [true, $destination];
}
return [false, "Bild konnte nicht auf dem Server gespeichert werden."];
$image_data = @file_get_contents($url, false, $context);
if ($image_data === false) return [false, "Bild-Download fehlgeschlagen."];
$image_info = @getimagesizefromstring($image_data);
if ($image_info === false) return [false, "Ungültiges Bild."];
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($image_info['mime'], $allowed)) return [false, "Format nicht unterstützt."];
$ext = image_type_to_extension($image_info[2], false);
$name = uniqid('img_url_', true) . '.' . $ext;
if (file_put_contents($upload_dir . $name, $image_data)) return [true, $upload_dir . $name];
return [false, "Speicherfehler."];
}
function save_image_from_base64($base64_string, $upload_dir) {
if (preg_match('/^data:image\/(\w+);base64,/', $base64_string, $type)) {
$data = substr($base64_string, strpos($base64_string, ',') + 1);
$type = strtolower($type[1]);
if (!in_array($type, ['jpg', 'jpeg', 'png', 'gif'])) { return [false, "Nicht unterstützter Bildtyp aus der Zwischenablage."]; }
if (!in_array($type, ['jpg', 'jpeg', 'png', 'gif'])) return [false, "Format nicht unterstützt."];
$data = base64_decode($data);
if ($data === false) { return [false, "Base64-Dekodierung fehlgeschlagen."]; }
} else { return [false, "Ungültiger Base64-String."]; }
$unique_file_name = uniqid('img_paste_', true) . '.' . $type;
$destination = $upload_dir . $unique_file_name;
if (file_put_contents($destination, $data)) { return [true, $destination]; }
return [false, "Bild aus der Zwischenablage konnte nicht gespeichert werden."];
if ($data === false) return [false, "Dekodierfehler."];
$name = uniqid('img_paste_', true) . '.' . $type;
if (file_put_contents($upload_dir . $name, $data)) return [true, $upload_dir . $name];
}
return [false, "Ungültiges Base64."];
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
@@ -138,6 +129,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
$product_url = trim($_POST['product_url']);
$product_designation = trim($_POST['product_designation']);
$pasted_image_data = $_POST['pasted_image_data'] ?? '';
$selected_existing_image = trim($_POST['selected_existing_image'] ?? '');
if (isset($_POST['manufacturer_id']) && $_POST['manufacturer_id'] === 'new') {
$new_manufacturer_name = trim($_POST['new_manufacturer_name']);
@@ -173,6 +165,10 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
$image_url_for_db = $destination;
} else { $image_error = "Fehler beim Verschieben der hochgeladenen Datei."; }
}
elseif (!empty($selected_existing_image)) {
// User chose an existing image
$image_url_for_db = $selected_existing_image;
}
elseif (!empty($image_url_from_input)) {
list($success, $result) = save_image_from_url($image_url_from_input, $upload_dir);
if ($success) { $image_url_for_db = $result; } else { $image_error = $result; }
@@ -213,10 +209,6 @@ $conn->close();
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
<!-- Tom Select CSS/JS -->
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0"><i class="fas fa-plus-circle me-2"></i>Neuen Artikel erstellen</h2>
@@ -226,6 +218,7 @@ $conn->close();
<?php if(!empty($message)) echo $message; ?>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post" enctype="multipart/form-data">
<input type="hidden" name="pasted_image_data" id="pasted_image_data">
<input type="hidden" name="selected_existing_image" id="selected_existing_image">
<div class="row g-4">
<div class="col-lg-7 d-flex flex-column">
<div class="card section-card mb-4">
@@ -272,6 +265,16 @@ $conn->close();
<div class="card-header"><h5 class="mb-0"><i class="fas fa-image me-2"></i>Bild & Produktseite</h5></div>
<div class="card-body d-flex flex-column">
<div class="mb-3"><label for="product_url" class="form-label">Produktseite (URL)</label><input type="url" class="form-control" id="product_url" name="product_url" value="<?php echo htmlspecialchars($product_url); ?>" placeholder="https://..."></div>
<!-- New: Button to open modal -->
<?php if(!empty($all_images)): ?>
<div class="mb-3">
<button type="button" class="btn btn-outline-secondary w-100" data-bs-toggle="modal" data-bs-target="#imageLibraryModal">
<i class="fas fa-images me-2"></i>Aus vorhandenen Bildern wählen
</button>
</div>
<?php endif; ?>
<div class="mb-3"><label for="image_file" class="form-label">1. Bild hochladen</label><input class="form-control" type="file" id="image_file" name="image_file" accept="image/*"></div>
<div class="mb-3"><label for="image_url" class="form-label">2. ODER Bild-URL angeben</label><input type="url" class="form-control" id="image_url" name="image_url" placeholder="https://..."></div>
<div class="mb-3"><label class="form-label">3. ODER Bild einfügen (Strg+V)</label><div id="pasteArea" class="paste-area"><i class="fas fa-paste"></i> Hier klicken & einfügen</div></div>
@@ -287,34 +290,114 @@ $conn->close();
</form>
</div>
</div>
<!-- Image Library Modal -->
<div class="modal fade" id="imageLibraryModal" tabindex="-1" aria-labelledby="imageLibraryModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageLibraryModalLabel">Bildbibliothek</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body bg-light">
<p class="small text-muted mb-2">Wähle ein Bild aus deinem bisherigen Bestand:</p>
<div class="d-flex flex-wrap gap-2 justify-content-center">
<?php if(!empty($all_images)): ?>
<?php foreach($all_images as $img): ?>
<div class="library-image-container" onclick="selectLibraryImage('<?php echo htmlspecialchars($img['image_url']); ?>')">
<img src="<?php echo htmlspecialchars($img['image_url']); ?>" loading="lazy">
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="text-muted">Keine Bilder gefunden.</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<style>
.library-image-container {
width: 100px;
height: 100px;
border: 2px solid transparent;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
background: white;
transition: transform 0.1s, border-color 0.1s;
}
.library-image-container:hover {
border-color: var(--color-primary);
transform: scale(1.05);
z-index: 10;
}
.library-image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
<script>
// Global function for onclick event in modal
function selectLibraryImage(url) {
const imagePreview = document.getElementById('imagePreview');
const selectedExistingInput = document.getElementById('selected_existing_image');
const imageFileInput = document.getElementById('image_file');
const imageUrlInput = document.getElementById('image_url');
const pastedImageDataInput = document.getElementById('pasted_image_data');
// Update Preview & Input
imagePreview.src = url;
selectedExistingInput.value = url;
// Clear other inputs
imageFileInput.value = '';
imageUrlInput.value = '';
pastedImageDataInput.value = '';
// Close Modal
const modalEl = document.getElementById('imageLibraryModal');
const modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
}
document.addEventListener('DOMContentLoaded', function() {
function setupAddNewOption(selectId, containerId) {
const select = document.getElementById(selectId);
const container = document.getElementById(containerId);
if (!select || !container) return;
const input = container.querySelector('input');
function toggle() {
const isNew = select.value === 'new';
container.style.display = isNew ? 'block' : 'none';
const input = container.querySelector('input');
if (input) input.required = isNew;
if (!isNew && input) { input.value = ''; }
}
select.addEventListener('change', toggle);
toggle();
return toggle;
}
setupAddNewOption('manufacturer_id', 'new_manufacturer_container');
setupAddNewOption('category_id', 'new_category_container');
const toggleMan = setupAddNewOption('manufacturer_id', 'new_manufacturer_container');
const toggleCat = setupAddNewOption('category_id', 'new_category_container');
const imageFileInput = document.getElementById('image_file');
const imageUrlInput = document.getElementById('image_url');
const imagePreview = document.getElementById('imagePreview');
const pasteArea = document.getElementById('pasteArea');
const pastedImageDataInput = document.getElementById('pasted_image_data');
const selectedExistingInput = document.getElementById('selected_existing_image');
function clearOtherImageInputs(source) {
if (source !== 'file') imageFileInput.value = '';
if (source !== 'url') imageUrlInput.value = '';
if (source !== 'paste') pastedImageDataInput.value = '';
if (source !== 'existing') selectedExistingInput.value = '';
}
if(imageFileInput) {
imageFileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
@@ -325,6 +408,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
}
if (pasteArea) {
pasteArea.addEventListener('paste', function(e) {
e.preventDefault();
@@ -345,7 +429,10 @@ document.addEventListener('DOMContentLoaded', function() {
pasteArea.addEventListener('dragover', (e) => { e.preventDefault(); pasteArea.classList.add('hover'); });
pasteArea.addEventListener('dragleave', () => pasteArea.classList.remove('hover'));
pasteArea.addEventListener('drop', (e) => { e.preventDefault(); pasteArea.classList.remove('hover'); });
pasteArea.setAttribute('tabindex', '0');
pasteArea.addEventListener('click', () => pasteArea.focus());
}
const consumableCheckbox = document.getElementById('consumable');
const quantityOwnedInput = document.getElementById('quantity_owned');
consumableCheckbox.addEventListener('change', function() {
@@ -357,21 +444,26 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// Initialize Tom Select for searchable dropdowns
const tsOptions = {
// Initialize Tom Select
const tsOptionsBase = {
create: false,
sortField: { field: "text", direction: "asc" },
onChange: function(value) {
// Force trigger change event for the "New" option toggle logic
const event = new Event('change');
this.input.dispatchEvent(event);
}
sortField: { field: "text", direction: "asc" }
};
if(document.getElementById('manufacturer_id')) new TomSelect('#manufacturer_id', tsOptions);
if(document.getElementById('category_id')) new TomSelect('#category_id', tsOptions);
if(document.getElementById('parent_article_id')) new TomSelect('#parent_article_id', tsOptions);
if(document.getElementById('storage_location_id')) new TomSelect('#storage_location_id', tsOptions);
if(document.getElementById('manufacturer_id')) {
new TomSelect('#manufacturer_id', {
...tsOptionsBase,
onChange: function(value) { if(toggleMan) toggleMan(); }
});
}
if(document.getElementById('category_id')) {
new TomSelect('#category_id', {
...tsOptionsBase,
onChange: function(value) { if(toggleCat) toggleCat(); }
});
}
if(document.getElementById('parent_article_id')) new TomSelect('#parent_article_id', tsOptionsBase);
if(document.getElementById('storage_location_id')) new TomSelect('#storage_location_id', tsOptionsBase);
});
</script>
<?php require_once 'footer.php'; ?>
<?php require_once 'footer.php'; ?>

View File

@@ -1,6 +1,6 @@
<?php
// add_packing_list.php - Neue Packliste hinzufügen
// KORREKTUR: Die Verarbeitungslogik wurde an den Anfang der Datei verschoben, um die Weiterleitung zu reparieren.
// VALIDIERUNG: Verhindert doppelte Rucksack-Zuweisung (Server & Client)
if (session_status() == PHP_SESSION_NONE) {
session_start();
@@ -12,7 +12,8 @@ if (!isset($_SESSION['user_id'])) {
require_once 'db_connect.php';
require_once 'household_actions.php';
require_once 'backpack_utils.php'; // Shared backpack functions
require_once 'backpack_utils.php';
require_once 'packing_list_utils.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
@@ -26,13 +27,29 @@ $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) {
$stmt_u = $conn->prepare("SELECT id, username FROM users WHERE household_id = ?");
$stmt_u->bind_param("i", $household_id_for_user);
} else {
// Just the current user
$stmt_u = $conn->prepare("SELECT id, username FROM users WHERE id = ?");
$stmt_u->bind_param("i", $current_user_id);
}
@@ -46,12 +63,36 @@ $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 (ONLY if no template is selected)
$has_duplicate_backpacks = false;
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;
}
}
}
}
}
if (empty($name)) {
$message = '<div class="alert alert-danger" role="alert">Der Packlistenname darf nicht leer sein.</div>';
} 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 {
@@ -59,35 +100,37 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
if ($stmt->execute()) {
$new_list_id = $conn->insert_id;
// Handle Backpacks and Participation
if (isset($_POST['participate']) && is_array($_POST['participate'])) {
foreach ($_POST['participate'] as $uid => $val) {
$uid = intval($uid);
// Only add if checked (value is usually '1')
if ($val) {
$bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0;
// Insert Carrier (even if no backpack is assigned, they are a carrier on the list)
// But DB schema: packing_list_carriers links user to list. Backpack is optional.
// If we want them on the list, we insert.
// Check if $bid is valid (>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);
}
}
}
}
} else {
// If no array (e.g. single user form without checkboxes?), usually we force current user?
// Or assume no one selected. Let's ensure Current User is added if not explicitly unchecked?
// Actually, the UI below will default to checked for everyone.
$_SESSION['message'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich erstellt!</div>';
}
if ($household_id_for_user) {
@@ -95,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 {
@@ -117,13 +159,28 @@ require_once 'header.php';
<div class="card-body p-4">
<?php if(!empty($message)) echo $message; ?>
<form action="add_packing_list.php" method="post">
<form action="add_packing_list.php" method="post" id="createListForm">
<div class="row">
<div class="col-md-7">
<div class="mb-3">
<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>
@@ -137,55 +194,140 @@ 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>
<?php
$user_backpacks_json = [];
$all_assigned_backpack_ids = []; // Empty for new list
foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $household_id_for_user);
// Pre-select: None (0)
$current_bp_id = 0;
// Store for JS
$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" 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">
<?php echo render_backpack_card_selector($user, $current_bp_id, $user_backpacks); ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary"><i class="fas fa-plus-circle me-2"></i>Packliste erstellen</button>
<a href="packing_lists.php" class="btn btn-secondary"><i class="fas fa-arrow-left me-2"></i>Abbrechen</a>
</div>
</form>
</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>
<!-- Render Modal JS -->
<?php echo render_backpack_modal_script($user_backpacks_json, $all_assigned_backpack_ids); ?>
<?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>
<hr>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary" id="submitBtn"><i class="fas fa-plus-circle me-2"></i>Packliste erstellen</button>
<a href="packing_lists.php" class="btn btn-secondary"><i class="fas fa-arrow-left me-2"></i>Abbrechen</a>
</div>
</form>
</div>
</div>
<!-- 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
const inputs = document.querySelectorAll('input[name^="backpacks["]');
inputs.forEach(input => {
const userIdMatch = input.name.match(/backpacks\[(\d+)\]/);
if (userIdMatch) {
const userId = userIdMatch[1];
const partCheck = document.getElementById('part_' + userId);
if (partCheck && partCheck.checked) {
const val = parseInt(input.value);
if (val > 0) {
if (selected.includes(val)) {
hasDuplicate = true;
}
selected.push(val);
}
}
}
});
if (hasDuplicate) {
warning.classList.remove('d-none');
submitBtn.disabled = true;
} else {
warning.classList.add('d-none');
submitBtn.disabled = false;
}
}
// Also listen to participation checkboxes
document.querySelectorAll('.participation-check').forEach(chk => {
chk.addEventListener('change', validateBackpacks);
});
document.body.addEventListener('hidden.bs.modal', function (event) {
validateBackpacks();
});
// Initial check
toggleParticipants();
});
</script>
<?php echo render_backpack_modal_script($user_backpacks_json, $all_assigned_backpack_ids); ?>
<?php require_once 'footer.php'; ?>
<?php require_once 'footer.php'; ?>

View File

@@ -224,7 +224,33 @@ function user_can_edit_list($conn, $packing_list_id, $user_id) {
}
function get_all_items($conn, $packing_list_id) {
$stmt = $conn->prepare("SELECT pli.id, pli.article_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) as name, a.weight_grams, a.product_designation, a.consumable, m.name as manufacturer_name FROM packing_list_items pli LEFT JOIN articles a ON pli.article_id = a.id LEFT JOIN manufacturers m ON a.manufacturer_id = m.id WHERE pli.packing_list_id = ? ORDER BY pli.order_index ASC, pli.id ASC");
$sql = "SELECT
pli.id,
pli.article_id,
pli.quantity,
pli.parent_packing_list_item_id,
pli.carrier_user_id,
pli.backpack_id,
pli.backpack_compartment_id,
COALESCE(
a.name,
bc.name,
CASE WHEN b.name IS NOT NULL THEN CONCAT('Rucksack: ', b.name) ELSE NULL END,
pli.name
) as name,
a.weight_grams,
a.product_designation,
a.consumable,
m.name as manufacturer_name
FROM packing_list_items pli
LEFT JOIN articles a ON pli.article_id = a.id
LEFT JOIN manufacturers m ON a.manufacturer_id = m.id
LEFT JOIN backpacks b ON pli.backpack_id = b.id
LEFT JOIN backpack_compartments bc ON pli.backpack_compartment_id = bc.id
WHERE pli.packing_list_id = ?
ORDER BY pli.order_index ASC, pli.id ASC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);

View File

@@ -6,9 +6,6 @@ function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
$bp = $conn->query("SELECT name FROM backpacks WHERE id = $backpack_id")->fetch_assoc();
// 2. Check/Create Root Item
// Use NULL safe comparison or check for NULL explicitly
// We need to allow for existing items that might be named differently if user renamed them?
// No, we stick to structure.
$root_id = 0;
$stmt = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id = ? AND backpack_id = ?");
$stmt->bind_param("iii", $list_id, $user_id, $backpack_id);
@@ -28,7 +25,8 @@ function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
}
// 3. Sync Compartments
$comps = $conn->query("SELECT id, name FROM backpack_compartments WHERE backpack_id = $backpack_id ORDER BY sort_order ASC");
// FIX: Fetch linked_article_id as well
$comps = $conn->query("SELECT id, name, linked_article_id FROM backpack_compartments WHERE backpack_id = $backpack_id ORDER BY sort_order ASC");
while ($comp = $comps->fetch_assoc()) {
// Check if item exists for this compartment AND this user
$stmt_c = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND backpack_compartment_id = ? AND carrier_user_id = ?");
@@ -36,9 +34,12 @@ function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
$stmt_c->execute();
if ($stmt_c->get_result()->num_rows == 0) {
// Create Compartment Item
$c_name = $comp['name'];
$stmt_ins_c = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, carrier_user_id, name, backpack_compartment_id, parent_packing_list_item_id, quantity, article_id, order_index) VALUES (?, ?, ?, ?, ?, 1, NULL, 0)");
$stmt_ins_c->bind_param("iisii", $list_id, $user_id, $c_name, $comp['id'], $root_id);
$c_name = $comp['name'];
$c_article_id = !empty($comp['linked_article_id']) ? $comp['linked_article_id'] : NULL;
// FIX: Insert linked article_id if present
$stmt_ins_c = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, carrier_user_id, name, backpack_compartment_id, parent_packing_list_item_id, quantity, article_id, order_index) VALUES (?, ?, ?, ?, ?, 1, ?, 0)");
$stmt_ins_c->bind_param("iisiii", $list_id, $user_id, $c_name, $comp['id'], $root_id, $c_article_id);
$stmt_ins_c->execute();
}
}
@@ -192,6 +193,28 @@ function render_backpack_modal_script($user_backpacks_json, $all_assigned_backpa
}
function selectBackpack(userId, bpId) {
// Update Logic Data first
const oldId = userBackpacksData[userId].current_id;
userBackpacksData[userId].current_id = bpId;
// Update Global Assigned List
// 1. Remove old ID if it was > 0
if (oldId > 0) {
const index = allAssignedIds.indexOf(parseInt(oldId)); // Ensure type matching
if (index > -1) {
allAssignedIds.splice(index, 1);
} else {
// Try string matching if int failed
const indexStr = allAssignedIds.indexOf(String(oldId));
if (indexStr > -1) allAssignedIds.splice(indexStr, 1);
}
}
// 2. Add new ID if > 0
if (bpId > 0) {
allAssignedIds.push(parseInt(bpId));
}
// Update Hidden Input
document.getElementById('input_bp_' + userId).value = bpId;

View File

@@ -32,8 +32,18 @@ if (isset($_POST['delete_backpack_id'])) {
}
}
// AUTO-MIGRATION: Check columns to prevent SQL errors
$check_col = $conn->query("SHOW COLUMNS FROM backpacks LIKE 'product_url'");
if ($check_col && $check_col->num_rows == 0) {
$conn->query("ALTER TABLE backpacks ADD COLUMN product_url VARCHAR(255) DEFAULT NULL AFTER image_url");
}
$check_col_c = $conn->query("SHOW COLUMNS FROM backpack_compartments LIKE 'linked_article_id'");
if ($check_col_c && $check_col_c->num_rows == 0) {
$conn->query("ALTER TABLE backpack_compartments ADD COLUMN linked_article_id INT DEFAULT NULL");
$conn->query("ALTER TABLE backpack_compartments ADD CONSTRAINT fk_bc_article FOREIGN KEY (linked_article_id) REFERENCES articles(id) ON DELETE SET NULL");
}
// Fetch Backpacks (Personal + Household)
// Logic: Show my backpacks AND backpacks from my household (if I'm in one)
$household_id = null;
$stmt_hh = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_hh->bind_param("i", $user_id);
@@ -44,7 +54,12 @@ if ($row = $res_hh->fetch_assoc()) {
}
$backpacks = [];
$sql = "SELECT b.*, u.username as owner_name
$sql = "SELECT b.*, u.username as owner_name,
(SELECT SUM(a.weight_grams)
FROM backpack_compartments bc
JOIN articles a ON bc.linked_article_id = a.id
WHERE bc.backpack_id = b.id) as extra_weight
FROM backpacks b
JOIN users u ON b.user_id = u.id
WHERE b.user_id = ?";
@@ -65,7 +80,6 @@ $result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
$backpacks[] = $row;
}
?>
<div class="card">
@@ -82,7 +96,9 @@ while ($row = $result->fetch_assoc()) {
</div>
<?php else: ?>
<div class="row g-4">
<?php foreach ($backpacks as $bp): ?>
<?php foreach ($backpacks as $bp):
$total_bp_weight = $bp['weight_grams'] + ($bp['extra_weight'] ?? 0);
?>
<div class="col-md-6 col-lg-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
@@ -106,13 +122,18 @@ while ($row = $result->fetch_assoc()) {
</p>
<div class="d-flex justify-content-between mb-3">
<span><i class="fas fa-weight-hanging text-muted me-1"></i> <?php echo $bp['weight_grams']; ?> g</span>
<span>
<i class="fas fa-weight-hanging text-muted me-1"></i>
<?php echo $total_bp_weight; ?> g
<?php if(($bp['extra_weight'] ?? 0) > 0): ?>
<small class="text-muted" title="Basis: <?php echo $bp['weight_grams']; ?>g + Taschen: <?php echo $bp['extra_weight']; ?>g">(+<?php echo $bp['extra_weight']; ?>)</small>
<?php endif; ?>
</span>
<span><i class="fas fa-box-open text-muted me-1"></i> <?php echo $bp['volume_liters']; ?> L</span>
</div>
<!-- Compartments Preview -->
<?php
// Fetch compartment count
$stmt_c = $conn->prepare("SELECT COUNT(*) as cnt FROM backpack_compartments WHERE backpack_id = ?");
$stmt_c->bind_param("i", $bp['id']);
$stmt_c->execute();
@@ -121,16 +142,24 @@ while ($row = $result->fetch_assoc()) {
<p class="small text-muted"><i class="fas fa-layer-group me-1"></i> <?php echo $cnt; ?> Fächer definiert</p>
</div>
<div class="card-footer bg-transparent border-top-0 d-flex justify-content-end gap-2">
<?php if ($bp['user_id'] == $user_id): ?>
<a href="edit_backpack.php?id=<?php echo $bp['id']; ?>" class="btn btn-sm btn-outline-primary"><i class="fas fa-edit"></i> Bearbeiten</a>
<form method="post" onsubmit="return confirm('Rucksack wirklich löschen?');" class="d-inline">
<input type="hidden" name="delete_backpack_id" value="<?php echo $bp['id']; ?>">
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash"></i></button>
</form>
<?php else: ?>
<button class="btn btn-sm btn-outline-secondary" disabled>Nur Eigentümer kann bearbeiten</button>
<?php endif; ?>
<div class="card-footer bg-transparent border-top-0 d-flex justify-content-between align-items-center gap-2">
<div>
<?php if (!empty($bp['product_url'])): ?>
<a href="<?php echo htmlspecialchars($bp['product_url']); ?>" target="_blank" class="btn btn-sm btn-outline-success" title="Herstellerseite öffnen"><i class="fas fa-external-link-alt"></i> Info</a>
<?php endif; ?>
</div>
<div class="d-flex gap-1">
<?php if ($bp['user_id'] == $user_id): ?>
<a href="edit_backpack.php?id=<?php echo $bp['id']; ?>" class="btn btn-sm btn-outline-primary"><i class="fas fa-edit"></i></a>
<form method="post" onsubmit="return confirm('Rucksack wirklich löschen?');" class="d-inline">
<input type="hidden" name="delete_backpack_id" value="<?php echo $bp['id']; ?>">
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash"></i></button>
</form>
<?php else: ?>
<button class="btn btn-sm btn-outline-secondary" disabled><i class="fas fa-lock"></i></button>
<?php endif; ?>
</div>
</div>
</div>
</div>
@@ -140,4 +169,4 @@ while ($row = $result->fetch_assoc()) {
</div>
</div>
<?php require_once 'footer.php'; ?>
<?php require_once 'footer.php'; ?>

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

@@ -23,6 +23,23 @@ $backpack = null;
$compartments = [];
$message = '';
$image_url = '';
$product_url = '';
// AUTO-MIGRATION: Check columns
$check_col = $conn->query("SHOW COLUMNS FROM backpacks LIKE 'product_url'");
if ($check_col && $check_col->num_rows == 0) {
$conn->query("ALTER TABLE backpacks ADD COLUMN product_url VARCHAR(255) DEFAULT NULL AFTER image_url");
}
$check_col_cat = $conn->query("SHOW COLUMNS FROM backpacks LIKE 'category_id'");
if ($check_col_cat && $check_col_cat->num_rows == 0) {
$conn->query("ALTER TABLE backpacks ADD COLUMN category_id INT DEFAULT NULL");
$conn->query("ALTER TABLE backpacks ADD CONSTRAINT fk_bp_category FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL");
}
$check_col_c = $conn->query("SHOW COLUMNS FROM backpack_compartments LIKE 'linked_article_id'");
if ($check_col_c && $check_col_c->num_rows == 0) {
$conn->query("ALTER TABLE backpack_compartments ADD COLUMN linked_article_id INT DEFAULT NULL");
$conn->query("ALTER TABLE backpack_compartments ADD CONSTRAINT fk_bc_article FOREIGN KEY (linked_article_id) REFERENCES articles(id) ON DELETE SET NULL");
}
// Check Household
$household_id = null;
@@ -75,6 +92,7 @@ if ($backpack_id > 0) {
if ($result->num_rows > 0) {
$backpack = $result->fetch_assoc();
$image_url = $backpack['image_url'];
$product_url = $backpack['product_url'] ?? '';
// Load Compartments
$stmt_c = $conn->prepare("SELECT * FROM backpack_compartments WHERE backpack_id = ? ORDER BY sort_order ASC");
@@ -90,14 +108,43 @@ if ($backpack_id > 0) {
}
}
// Load Manufacturers
$stmt_man_load = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id = ? ORDER BY name ASC");
$stmt_man_load->bind_param("i", $user_id);
// Load Manufacturers & Categories
$hh_ids = [$user_id];
if ($household_id) {
$stmt_hhm = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_hhm->bind_param("i", $household_id);
$stmt_hhm->execute();
$res_hhm = $stmt_hhm->get_result();
while($r = $res_hhm->fetch_assoc()) $hh_ids[] = $r['id'];
}
$placeholders = implode(',', array_fill(0, count($hh_ids), '?'));
$types_hh = str_repeat('i', count($hh_ids));
// Manufacturers
$stmt_man_load = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_man_load->bind_param($types_hh, ...$hh_ids);
$stmt_man_load->execute();
$manufacturers = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_man_load->close();
// Handle Form Submission BEFORE loading header
// Categories
$stmt_cat_load = $conn->prepare("SELECT id, name FROM categories WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_cat_load->bind_param($types_hh, ...$hh_ids);
$stmt_cat_load->execute();
$categories = $stmt_cat_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_cat_load->close();
// Load Articles for Linked Compartments
$stmt_arts = $conn->prepare("SELECT id, name, weight_grams, image_url FROM articles WHERE user_id IN ($placeholders) OR household_id = ? ORDER BY name ASC");
$params_arts = array_merge($hh_ids, [$household_id ?: 0]);
$types_arts = $types_hh . 'i';
$stmt_arts->bind_param($types_arts, ...$params_arts);
$stmt_arts->execute();
$all_articles = $stmt_arts->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_arts->close();
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$name = trim($_POST['name']);
@@ -107,14 +154,21 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if ($_POST['manufacturer_select'] === 'new') {
$new_man = trim($_POST['new_manufacturer_name']);
if (!empty($new_man)) {
// Optional: Save to manufacturers table for future use
$stmt_new_man = $conn->prepare("INSERT INTO manufacturers (name, user_id) VALUES (?, ?)");
$stmt_new_man->bind_param("si", $new_man, $user_id);
$stmt_new_man->execute();
$manufacturer = $new_man;
$stmt_check_man = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id = ? AND name = ?");
$stmt_check_man->bind_param("is", $user_id, $new_man);
$stmt_check_man->execute();
$res_check = $stmt_check_man->get_result();
if ($res_check->num_rows > 0) {
$manufacturer = $res_check->fetch_assoc()['name'];
} else {
$stmt_new_man = $conn->prepare("INSERT INTO manufacturers (name, user_id) VALUES (?, ?)");
$stmt_new_man->bind_param("si", $new_man, $user_id);
$stmt_new_man->execute();
$manufacturer = $new_man;
}
}
} else {
// Look up name from ID
$man_id = intval($_POST['manufacturer_select']);
foreach ($manufacturers as $m) {
if ($m['id'] == $man_id) {
@@ -129,77 +183,50 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$weight = intval($_POST['weight_grams']);
$volume = intval($_POST['volume_liters']);
$share_household = isset($_POST['share_household']) ? 1 : 0;
$product_url_input = trim($_POST['product_url'] ?? '');
$category_id = !empty($_POST['category_id']) ? intval($_POST['category_id']) : NULL;
// Image Handling
$image_url_for_db = $image_url; // Keep existing by default
$image_url_for_db = $image_url;
$pasted_image = $_POST['pasted_image_data'] ?? '';
$url_image = trim($_POST['image_url_input'] ?? '');
if (!empty($pasted_image)) {
list($ok, $res) = save_image_from_base64($pasted_image, $upload_dir);
if ($ok) {
$image_url_for_db = $res;
} else {
$message .= '<div class="alert alert-warning">Fehler beim Speichern des eingefügten Bildes: ' . htmlspecialchars($res) . '</div>';
}
} elseif (isset($_FILES['image_file']) && $_FILES['image_file']['error'] != UPLOAD_ERR_NO_FILE) {
// User attempted to upload a file
if ($_FILES['image_file']['error'] == UPLOAD_ERR_OK) {
$ext = strtolower(pathinfo($_FILES['image_file']['name'], PATHINFO_EXTENSION));
$allowed_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($ext, $allowed_exts)) {
$name_file = uniqid('bp_img_', true) . '.' . $ext;
if (move_uploaded_file($_FILES['image_file']['tmp_name'], $upload_dir . $name_file)) {
$image_url_for_db = $upload_dir . $name_file;
} else {
$message .= '<div class="alert alert-danger">Fehler beim Verschieben der Datei. Schreibrechte prüfen.</div>';
}
} else {
$message .= '<div class="alert alert-warning">Ungültiges Dateiformat. Erlaubt: JPG, PNG, GIF, WEBP.</div>';
if ($ok) $image_url_for_db = $res;
} elseif (isset($_FILES['image_file']) && $_FILES['image_file']['error'] == UPLOAD_ERR_OK) {
$ext = strtolower(pathinfo($_FILES['image_file']['name'], PATHINFO_EXTENSION));
$allowed_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($ext, $allowed_exts)) {
$name_file = uniqid('bp_img_', true) . '.' . $ext;
if (move_uploaded_file($_FILES['image_file']['tmp_name'], $upload_dir . $name_file)) {
$image_url_for_db = $upload_dir . $name_file;
}
} else {
// Handle upload errors
$err_code = $_FILES['image_file']['error'];
$err_msg = 'Unbekannter Fehler';
switch ($err_code) {
case UPLOAD_ERR_INI_SIZE: $err_msg = 'Datei ist zu groß (php.ini limit).'; break;
case UPLOAD_ERR_FORM_SIZE: $err_msg = 'Datei ist zu groß (HTML form limit).'; break;
case UPLOAD_ERR_PARTIAL: $err_msg = 'Datei wurde nur teilweise hochgeladen.'; break;
case UPLOAD_ERR_NO_TMP_DIR: $err_msg = 'Kein temporärer Ordner gefunden.'; break;
case UPLOAD_ERR_CANT_WRITE: $err_msg = 'Fehler beim Schreiben auf die Festplatte.'; break;
}
$message .= '<div class="alert alert-danger">Upload-Fehler: ' . $err_msg . '</div>';
}
} elseif (!empty($url_image)) {
list($ok, $res) = save_image_from_url($url_image, $upload_dir);
if ($ok) {
$image_url_for_db = $res;
} else {
$message .= '<div class="alert alert-warning">Fehler beim Laden von URL: ' . htmlspecialchars($res) . '</div>';
}
if ($ok) $image_url_for_db = $res;
}
$final_household_id = ($share_household && $household_id) ? $household_id : NULL;
if ($backpack_id > 0) {
// Update
$stmt = $conn->prepare("UPDATE backpacks SET name=?, manufacturer=?, model=?, weight_grams=?, volume_liters=?, household_id=?, image_url=? WHERE id=? AND user_id=?");
$stmt->bind_param("sssiissii", $name, $manufacturer, $model, $weight, $volume, $final_household_id, $image_url_for_db, $backpack_id, $user_id);
$stmt = $conn->prepare("UPDATE backpacks SET name=?, manufacturer=?, model=?, weight_grams=?, volume_liters=?, household_id=?, image_url=?, product_url=?, category_id=? WHERE id=? AND user_id=?");
$stmt->bind_param("sssiisssiii", $name, $manufacturer, $model, $weight, $volume, $final_household_id, $image_url_for_db, $product_url_input, $category_id, $backpack_id, $user_id);
$stmt->execute();
} else {
// Insert
$stmt = $conn->prepare("INSERT INTO backpacks (user_id, household_id, name, manufacturer, model, weight_grams, volume_liters, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("iisssiis", $user_id, $final_household_id, $name, $manufacturer, $model, $weight, $volume, $image_url_for_db);
$stmt = $conn->prepare("INSERT INTO backpacks (user_id, household_id, name, manufacturer, model, weight_grams, volume_liters, image_url, product_url, category_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("iisssiissi", $user_id, $final_household_id, $name, $manufacturer, $model, $weight, $volume, $image_url_for_db, $product_url_input, $category_id);
$stmt->execute();
$backpack_id = $stmt->insert_id;
}
// Handle Compartments
if (isset($_POST['compartment_names'])) {
$comp_names = $_POST['compartment_names'];
$comp_ids = $_POST['compartment_ids'] ?? [];
if (isset($_POST['comp_types'])) {
$types = $_POST['comp_types'];
$ids = $_POST['comp_ids'] ?? [];
$names = $_POST['comp_names'] ?? [];
$articles = $_POST['comp_articles'] ?? [];
// Get existing IDs to know what to delete
$existing_ids = [];
if($backpack_id > 0){
$stmt_check = $conn->prepare("SELECT id FROM backpack_compartments WHERE backpack_id = ?");
@@ -211,27 +238,38 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$kept_ids = [];
for ($i = 0; $i < count($comp_names); $i++) {
$c_name = trim($comp_names[$i]);
$c_id = intval($comp_ids[$i] ?? 0);
if (empty($c_name)) continue;
for ($i = 0; $i < count($types); $i++) {
$c_id = intval($ids[$i] ?? 0);
$c_type = $types[$i];
$c_name = '';
$c_article_id = NULL;
if ($c_type === 'article') {
$c_article_id = intval($articles[$i] ?? 0);
if ($c_article_id <= 0) continue;
foreach($all_articles as $art) {
if ($art['id'] == $c_article_id) {
$c_name = $art['name'];
break;
}
}
} else {
$c_name = trim($names[$i] ?? '');
if (empty($c_name)) continue;
}
if ($c_id > 0 && in_array($c_id, $existing_ids)) {
// Update
$stmt_up = $conn->prepare("UPDATE backpack_compartments SET name = ?, sort_order = ? WHERE id = ?");
$stmt_up->bind_param("sii", $c_name, $i, $c_id);
$stmt_up = $conn->prepare("UPDATE backpack_compartments SET name = ?, sort_order = ?, linked_article_id = ? WHERE id = ?");
$stmt_up->bind_param("siii", $c_name, $i, $c_article_id, $c_id);
$stmt_up->execute();
$kept_ids[] = $c_id;
} else {
// Insert
$stmt_in = $conn->prepare("INSERT INTO backpack_compartments (backpack_id, name, sort_order) VALUES (?, ?, ?)");
$stmt_in->bind_param("isi", $backpack_id, $c_name, $i);
$stmt_in = $conn->prepare("INSERT INTO backpack_compartments (backpack_id, name, sort_order, linked_article_id) VALUES (?, ?, ?, ?)");
$stmt_in->bind_param("isii", $backpack_id, $c_name, $i, $c_article_id);
$stmt_in->execute();
}
}
// Delete removed
foreach ($existing_ids as $ex_id) {
if (!in_array($ex_id, $kept_ids)) {
$conn->query("DELETE FROM backpack_compartments WHERE id = $ex_id");
@@ -243,9 +281,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
exit;
}
// Handle Form Submission BEFORE loading header
// ... (existing logic) ...
require_once 'header.php';
?>
@@ -290,18 +325,39 @@ require_once 'header.php';
<input type="text" name="new_manufacturer_name" class="form-control form-control-sm" placeholder="Name des neuen Herstellers">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Kategorie</label>
<select class="form-select" id="category_id" name="category_id">
<option value="">-- Keine (Sonstiges) --</option>
<?php
$current_cat = $backpack['category_id'] ?? 0;
foreach ($categories as $cat):
$selected = ($cat['id'] == $current_cat) ? 'selected' : '';
?>
<option value="<?php echo $cat['id']; ?>" <?php echo $selected; ?>><?php echo htmlspecialchars($cat['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Modell</label>
<input type="text" name="model" class="form-control" value="<?php echo htmlspecialchars($backpack['model'] ?? ''); ?>">
</div>
<div class="col-md-6">
<label class="form-label">Leergewicht (g)</label>
<input type="number" name="weight_grams" class="form-control" value="<?php echo htmlspecialchars($backpack['weight_grams'] ?? 0); ?>">
<label class="form-label">Basis-Leergewicht (g)</label>
<input type="number" name="weight_grams" class="form-control" value="<?php echo htmlspecialchars($backpack['weight_grams'] ?? 0); ?>" required>
<div class="form-text small">Ohne Zusatztaschen.</div>
</div>
<div class="col-md-6">
<label class="form-label">Volumen (Liter)</label>
<input type="number" name="volume_liters" class="form-control" value="<?php echo htmlspecialchars($backpack['volume_liters'] ?? 0); ?>">
</div>
<div class="col-md-12">
<label class="form-label">Hersteller-Link (Produktseite)</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-link"></i></span>
<input type="url" name="product_url" class="form-control" value="<?php echo htmlspecialchars($product_url); ?>" placeholder="https://...">
</div>
</div>
<div class="col-md-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="share_household" name="share_household" <?php echo (!empty($backpack['household_id']) || $backpack_id == 0) ? 'checked' : ''; ?>>
@@ -323,43 +379,77 @@ require_once 'header.php';
</div>
</div>
<h5 class="border-bottom pb-2 mb-3">Fächeraufteilung</h5>
<h5 class="border-bottom pb-2 mb-3">Fächer & Zusatztaschen</h5>
<div class="alert alert-light small">
Definiere hier die Bereiche deines Rucksacks (z.B. Deckelfach, Bodenfach). Diese erscheinen später in der Packliste als Container.
Definiere hier die Bereiche deines Rucksacks. Du kannst einfache Text-Fächer (z.B. "Deckelfach") oder echte Artikel als Zusatztaschen (z.B. "Hüftgurttasche") hinzufügen. Das Gewicht von Zusatztaschen wird zum Rucksackgewicht addiert.
</div>
<div id="compartments-container">
<?php if (empty($compartments)): ?>
<!-- Default Compartments for new backpack -->
<div class="input-group mb-2 compartment-row">
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" value="Hauptfach" placeholder="Fachname">
<input type="hidden" name="compartment_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
</div>
<div class="input-group mb-2 compartment-row">
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" value="Deckelfach" placeholder="Fachname">
<input type="hidden" name="compartment_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
<!-- Defaults -->
<div class="compartment-row card mb-2">
<div class="card-body p-2">
<div class="input-group">
<span class="input-group-text handle cursor-grab"><i class="fas fa-grip-lines text-muted"></i></span>
<select class="form-select type-select" name="comp_types[]" style="max-width: 130px;">
<option value="text">Standardfach</option>
<option value="article">Zusatztasche</option>
</select>
<!-- Text Input -->
<input type="text" name="comp_names[]" class="form-control name-input" value="Hauptfach" placeholder="Fachname">
<!-- Article Select -->
<div class="flex-grow-1 article-select-wrapper" style="display:none;">
<select name="comp_articles[]" class="form-select article-select tom-select-init">
<option value="">Artikel wählen...</option>
<?php foreach($all_articles as $art): ?>
<option value="<?php echo $art['id']; ?>" data-src="<?php echo !empty($art['image_url']) ? htmlspecialchars($art['image_url']) : ''; ?>"><?php echo htmlspecialchars($art['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<input type="hidden" name="comp_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
</div>
</div>
</div>
<?php else: ?>
<?php foreach ($compartments as $comp):
// Ensure proper escaping for HTML attributes
$comp_name_escaped = htmlspecialchars($comp['name']);
$comp_id_escaped = htmlspecialchars($comp['id']);
$is_article = !empty($comp['linked_article_id']);
?>
<div class="input-group mb-2 compartment-row">
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" value="<?php echo $comp_name_escaped; ?>">
<input type="hidden" name="compartment_ids[]" value="<?php echo $comp_id_escaped; ?>">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
<div class="compartment-row card mb-2">
<div class="card-body p-2">
<div class="input-group">
<span class="input-group-text handle cursor-grab"><i class="fas fa-grip-lines text-muted"></i></span>
<select class="form-select type-select" name="comp_types[]" style="max-width: 130px;">
<option value="text" <?php echo !$is_article ? 'selected' : ''; ?>>Standardfach</option>
<option value="article" <?php echo $is_article ? 'selected' : ''; ?>>Zusatztasche</option>
</select>
<input type="text" name="comp_names[]" class="form-control name-input" value="<?php echo htmlspecialchars($comp['name']); ?>" style="<?php echo $is_article ? 'display:none;' : ''; ?>">
<div class="flex-grow-1 article-select-wrapper" style="<?php echo !$is_article ? 'display:none;' : ''; ?>">
<select name="comp_articles[]" class="form-select article-select">
<option value="">Artikel wählen...</option>
<?php foreach($all_articles as $art):
$sel = ($is_article && $art['id'] == $comp['linked_article_id']) ? 'selected' : '';
?>
<option value="<?php echo $art['id']; ?>" <?php echo $sel; ?> data-src="<?php echo !empty($art['image_url']) ? htmlspecialchars($art['image_url']) : ''; ?>"><?php echo htmlspecialchars($art['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<input type="hidden" name="comp_ids[]" value="<?php echo $comp['id']; ?>">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mb-4" id="add-compartment"><i class="fas fa-plus"></i> Fach hinzufügen</button>
<button type="button" class="btn btn-sm btn-outline-success mb-4" id="add-compartment"><i class="fas fa-plus"></i> Fach / Tasche hinzufügen</button>
<div class="d-flex justify-content-between border-top pt-3">
<a href="backpacks.php" class="btn btn-secondary">Abbrechen</a>
@@ -378,45 +468,134 @@ document.addEventListener('DOMContentLoaded', function() {
const manSelect = document.getElementById('manufacturer_select');
const manContainer = document.getElementById('new_manufacturer_container');
function toggleManContainer(val) {
if (val === 'new') {
manContainer.style.display = 'block';
const input = manContainer.querySelector('input');
if(input) input.required = true;
} else {
manContainer.style.display = 'none';
const input = manContainer.querySelector('input');
if(input) input.required = false;
}
}
if (manSelect) {
manSelect.addEventListener('change', function() {
if (this.value === 'new') {
manContainer.style.display = 'block';
manContainer.querySelector('input').required = true;
} else {
manContainer.style.display = 'none';
manContainer.querySelector('input').required = false;
}
});
toggleManContainer(manSelect.value);
manSelect.addEventListener('change', function() { toggleManContainer(this.value); });
new TomSelect(manSelect, { create: false, sortField: { field: "text", direction: "asc" }, onChange: function(value) { toggleManContainer(value); } });
}
// New: Category Select
if (document.getElementById('category_id')) {
new TomSelect('#category_id', { create: false, sortField: { field: "text", direction: "asc" } });
}
const container = document.getElementById('compartments-container');
// Sortable for Compartments
new Sortable(container, {
handle: '.input-group-text',
handle: '.handle',
animation: 150
});
// Template for new row (with placeholders)
const rowTemplate = `
<div class="compartment-row card mb-2">
<div class="card-body p-2">
<div class="input-group">
<span class="input-group-text handle cursor-grab"><i class="fas fa-grip-lines text-muted"></i></span>
<select class="form-select type-select" name="comp_types[]" style="max-width: 130px;">
<option value="text">Standardfach</option>
<option value="article">Zusatztasche</option>
</select>
<input type="text" name="comp_names[]" class="form-control name-input" placeholder="Fachname">
<div class="flex-grow-1 article-select-wrapper" style="display:none;">
<select name="comp_articles[]" class="form-select article-select tom-select-init">
<option value="">Artikel wählen...</option>
<?php foreach($all_articles as $art): ?>
<option value="<?php echo $art['id']; ?>" data-src="<?php echo !empty($art['image_url']) ? htmlspecialchars($art['image_url']) : ''; ?>"><?php echo htmlspecialchars($art['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<input type="hidden" name="comp_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
</div>
</div>
</div>
`;
document.getElementById('add-compartment').addEventListener('click', function() {
const div = document.createElement('div');
div.className = 'input-group mb-2 compartment-row';
div.innerHTML = `
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" placeholder="Fachname">
<input type="hidden" name="compartment_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
`;
container.appendChild(div);
container.insertAdjacentHTML('beforeend', rowTemplate);
const newRow = container.lastElementChild;
initRowLogic(newRow);
});
function initTomSelect(select) {
if (select.tomselect) return;
new TomSelect(select, {
create: false,
sortField: { field: "text", direction: "asc" },
dropdownParent: 'body',
render: {
option: function(data, escape) {
let src = data.src || '';
return `<div>
${src ? `<img src="${src}" style="width:24px;height:24px;object-fit:cover;margin-right:8px;vertical-align:middle;">` : ''}
<span>${escape(data.text)}</span>
</div>`;
},
item: function(data, escape) {
let src = data.src || '';
return `<div>
${src ? `<img src="${src}" style="width:20px;height:20px;object-fit:cover;margin-right:6px;vertical-align:middle;">` : ''}
<span>${escape(data.text)}</span>
</div>`;
}
}
});
}
function initRowLogic(row) {
const typeSelect = row.querySelector('.type-select');
const nameInput = row.querySelector('.name-input');
const artWrapper = row.querySelector('.article-select-wrapper');
const artSelect = row.querySelector('.article-select');
typeSelect.addEventListener('change', function() {
if (this.value === 'article') {
nameInput.style.display = 'none';
artWrapper.style.display = 'block';
if (artSelect.classList.contains('tom-select-init') && !artSelect.tomselect) {
initTomSelect(artSelect);
artSelect.classList.remove('tom-select-init');
} else if (artSelect && !artSelect.tomselect) {
initTomSelect(artSelect);
}
} else {
nameInput.style.display = 'block';
artWrapper.style.display = 'none';
}
});
// Init if already visible (editing mode)
if (artSelect && !artSelect.tomselect && artWrapper.style.display !== 'none') {
initTomSelect(artSelect);
}
}
// Init existing rows
container.querySelectorAll('.compartment-row').forEach(initRowLogic);
container.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-comp')) {
e.target.closest('.compartment-row').remove();
}
});
// Image Handling Logic (copied from add_article.php pattern)
// Image Handling Logic
const imageFileInput = document.getElementById('image_file');
const imagePreview = document.getElementById('imagePreview');
const pasteArea = document.getElementById('pasteArea');
@@ -433,7 +612,6 @@ document.addEventListener('DOMContentLoaded', function() {
}
if (pasteArea) {
// ... existing paste logic ...
pasteArea.addEventListener('paste', function(e) {
e.preventDefault();
const items = (e.clipboardData || window.clipboardData).items;
@@ -451,23 +629,9 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});
// Add focus to paste area on click so it can receive paste events
pasteArea.setAttribute('tabindex', '0');
pasteArea.addEventListener('click', () => pasteArea.focus());
}
// Tom Select Init
const manSelect = document.getElementById('manufacturer_select');
if(manSelect) {
new TomSelect(manSelect, {
create: false,
sortField: { field: "text", direction: "asc" },
onChange: function(value) {
const event = new Event('change');
manSelect.dispatchEvent(event);
}
});
}
});
</script>

View File

@@ -12,7 +12,7 @@ if (!isset($_SESSION['user_id'])) {
require_once 'db_connect.php';
require_once 'household_actions.php';
require_once 'backpack_utils.php'; // Fix: Include utils
require_once 'backpack_utils.php';
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];
@@ -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();
@@ -85,61 +85,73 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
// Update Basic Details
$name = trim($_POST['name']);
$description = trim($_POST['description']);
$stmt_update = $conn->prepare("UPDATE packing_lists SET name = ?, description = ? WHERE id = ?");
$stmt_update->bind_param("ssi", $name, $description, $packing_list_id);
// Household sharing logic
$new_household_id = NULL;
if (isset($_POST['is_household_list']) && $_POST['is_household_list'] == '1' && $current_user_household_id) {
$new_household_id = $current_user_household_id;
}
$stmt_update = $conn->prepare("UPDATE packing_lists SET name = ?, description = ?, household_id = ? WHERE id = ?");
$stmt_update->bind_param("ssii", $name, $description, $new_household_id, $packing_list_id);
$stmt_update->execute();
$packing_list['name'] = $name;
$packing_list['description'] = $description;
$packing_list['household_id'] = $new_household_id;
// Handle Backpack Assignments
if (isset($_POST['backpacks'])) {
foreach ($_POST['backpacks'] as $uid => $bid) {
$uid = intval($uid);
$bid = intval($bid);
// Handle Participation & Backpacks
// Get all potential users to check if they were unchecked
$participate_map = $_POST['participate'] ?? []; // Array of UserID => "1" if checked
foreach ($available_users as $user) {
$uid = $user['id'];
$is_checked = isset($participate_map[$uid]);
$was_participating = array_key_exists($uid, $current_assignments);
if ($is_checked) {
// User participates -> Update/Insert Backpack
$bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0;
$bid = ($bid > 0) ? $bid : NULL;
// Update Carrier Table
// Check if exists
$old_backpack_id = 0;
$stmt_chk = $conn->prepare("SELECT id, backpack_id FROM packing_list_carriers WHERE packing_list_id = ? AND user_id = ?");
$stmt_chk->bind_param("ii", $packing_list_id, $uid);
$stmt_chk->execute();
$res_chk = $stmt_chk->get_result();
if ($row_chk = $res_chk->fetch_assoc()) {
$old_backpack_id = $row_chk['backpack_id'];
$stmt_up = $conn->prepare("UPDATE packing_list_carriers SET backpack_id = ? WHERE packing_list_id = ? AND user_id = ?");
$stmt_up->bind_param("iii", $bid, $packing_list_id, $uid);
$stmt_up->execute();
if ($was_participating) {
$old_bid = $current_assignments[$uid];
// Update if changed
if ($old_bid != $bid) {
$stmt_up = $conn->prepare("UPDATE packing_list_carriers SET backpack_id = ? WHERE packing_list_id = ? AND user_id = ?");
$stmt_up->bind_param("iii", $bid, $packing_list_id, $uid);
$stmt_up->execute();
// Cleanup Old Containers
cleanup_old_backpack_containers($conn, $packing_list_id, $uid);
// Sync New
if ($bid) {
sync_backpack_items($conn, $packing_list_id, $uid, $bid);
}
}
} else {
// New Participant
$stmt_in = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)");
$stmt_in->bind_param("iii", $packing_list_id, $uid, $bid);
$stmt_in->execute();
}
// CLEANUP LOGIC: If backpack changed or removed
if ($old_backpack_id != $bid) {
// 1. Unparent all items that are inside the old containers (so they don't get deleted)
// Find all container items for this user
$stmt_find_containers = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id = ? AND (backpack_id IS NOT NULL OR backpack_compartment_id IS NOT NULL)");
$stmt_find_containers->bind_param("ii", $packing_list_id, $uid);
$stmt_find_containers->execute();
$res_cont = $stmt_find_containers->get_result();
$container_ids = [];
while ($r = $res_cont->fetch_assoc()) $container_ids[] = $r['id'];
if (!empty($container_ids)) {
$ids_str = implode(',', $container_ids);
// Set parent to NULL for children of these containers
$conn->query("UPDATE packing_list_items SET parent_packing_list_item_id = NULL WHERE packing_list_id = $packing_list_id AND parent_packing_list_item_id IN ($ids_str)");
// 2. Delete the containers
$conn->query("DELETE FROM packing_list_items WHERE id IN ($ids_str)");
if ($bid) {
sync_backpack_items($conn, $packing_list_id, $uid, $bid);
}
}
// SYNC LOGIC (Only if new backpack assigned)
if ($bid && $old_backpack_id != $bid) {
sync_backpack_items($conn, $packing_list_id, $uid, $bid);
} else {
// User NOT checked -> Remove if existed
if ($was_participating) {
$conn->query("DELETE FROM packing_list_carriers WHERE packing_list_id = $packing_list_id AND user_id = $uid");
// Cleanup Items: Delete all items carried by this user?
// Or move to unassigned? Let's move to unassigned (NULL) to be safe against data loss.
// But Containers (Backpacks) should be deleted.
// 1. Delete Containers
cleanup_old_backpack_containers($conn, $packing_list_id, $uid);
// 2. Move remaining items (loose items) to unassigned?
$conn->query("UPDATE packing_list_items SET carrier_user_id = NULL WHERE packing_list_id = $packing_list_id AND carrier_user_id = $uid");
}
}
}
@@ -154,18 +166,41 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
}
}
function cleanup_old_backpack_containers($conn, $list_id, $user_id) {
// Find all container items for this user (backpacks or compartments)
$stmt = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id = ? AND (backpack_id IS NOT NULL OR backpack_compartment_id IS NOT NULL)");
$stmt->bind_param("ii", $list_id, $user_id);
$stmt->execute();
$res = $stmt->get_result();
$container_ids = [];
while ($r = $res->fetch_assoc()) $container_ids[] = $r['id'];
if (!empty($container_ids)) {
$ids_str = implode(',', $container_ids);
// Unparent children so they don't get deleted (or keep them and they get deleted? No, save content)
// Set parent to NULL for children of these containers
$conn->query("UPDATE packing_list_items SET parent_packing_list_item_id = NULL WHERE packing_list_id = $list_id AND parent_packing_list_item_id IN ($ids_str)");
// Delete containers
$conn->query("DELETE FROM packing_list_items WHERE id IN ($ids_str)");
}
}
$back_link = 'packing_lists.php' . (!empty($packing_list['is_template']) ? '?view=templates' : '');
$page_headline = !empty($packing_list['is_template']) ? 'Vorlage bearbeiten' : 'Details bearbeiten';
?>
<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>
<h2 class="h4 mb-0"><?php echo $page_headline; ?>: <?php echo htmlspecialchars($packing_list['name'] ?? ''); ?></h2>
<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; ?>
<?php if ($packing_list): ?>
<form method="post">
<form method="post" id="editListForm">
<div class="row">
<div class="col-md-8">
<h5 class="mb-3">Basisdaten</h5>
@@ -177,38 +212,51 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
<label class="form-label">Beschreibung</label>
<textarea class="form-control" name="description" rows="3"><?php echo htmlspecialchars($packing_list['description'] ?: ''); ?></textarea>
</div>
<?php if ($current_user_household_id): ?>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" role="switch" id="is_household_list" name="is_household_list" value="1" <?php echo !empty($packing_list['household_id']) ? 'checked' : ''; ?>>
<label class="form-check-label" for="is_household_list">Für den gesamten Haushalt freigeben</label>
</div>
<?php endif; ?>
</div>
<div class="col-md-4">
<h5 class="mb-3">Rucksack-Zuweisung</h5>
<h5 class="mb-3">Teilnehmer & Rucksäcke <?php if(!empty($packing_list['is_template'])): ?><small class="text-muted">(Standard)</small><?php endif; ?></h5>
<div class="card bg-light border-0">
<div class="card-body">
<p class="small text-muted">Wähle hier, wer welchen Rucksack trägt. Bereits vergebene Rucksäcke werden ausgeblendet.</p>
<p class="small text-muted">Wähle hier, wer mitkommt und welchen Rucksack er 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> Ein Rucksack wurde mehrfach ausgewählt!
</div>
<?php
// Calculate used backpacks
$all_assigned_backpack_ids = array_values($current_assignments);
// Prepare data for JS (still needed for the render helper)
$user_backpacks_json = [];
foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $packing_list['household_id']);
$is_participating = array_key_exists($user['id'], $current_assignments);
$my_current_bp_id = $current_assignments[$user['id']] ?? 0;
// Store for JS
$user_backpacks_json[$user['id']] = [
'current_id' => $my_current_bp_id,
'backpacks' => $user_backpacks
];
// Use helper to render the widget
echo '<div class="mb-4 border-bottom pb-3">';
echo '<label class="form-label fw-bold mb-2">' . htmlspecialchars($user['username']) . '</label>';
echo render_backpack_card_selector($user, $my_current_bp_id, $user_backpacks);
echo '</div>';
endforeach;
?>
<div class="mb-4 border-bottom pb-3">
<div class="form-check mb-2">
<input class="form-check-input participation-check" type="checkbox" name="participate[<?php echo $user['id']; ?>]" value="1" <?php echo $is_participating ? '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 backpack-selector-wrapper" style="<?php echo $is_participating ? '' : 'opacity: 0.5; pointer-events: none;'; ?>">
<?php echo render_backpack_card_selector($user, $my_current_bp_id, $user_backpacks); ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
@@ -218,7 +266,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
<div class="d-flex justify-content-between">
<a href="manage_packing_list_items.php?id=<?php echo $packing_list_id; ?>" class="btn btn-info text-white"><i class="fas fa-boxes me-2"></i>Inhalt bearbeiten</a>
<button type="submit" class="btn btn-primary"><i class="fas fa-save me-2"></i>Speichern & Synchronisieren</button>
<button type="submit" class="btn btn-primary" id="submitBtn"><i class="fas fa-save me-2"></i>Speichern & Synchronisieren</button>
</div>
</form>
<?php endif; ?>
@@ -228,4 +276,63 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
<!-- Render Modal JS -->
<?php echo render_backpack_modal_script($user_backpacks_json, $all_assigned_backpack_ids); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
const submitBtn = document.getElementById('submitBtn');
const warning = document.getElementById('backpack-warning');
// Toggle Opacity logic
document.querySelectorAll('.participation-check').forEach(chk => {
chk.addEventListener('change', function() {
const wrapper = this.closest('.mb-4').querySelector('.backpack-selector-wrapper');
if (this.checked) {
wrapper.style.opacity = '1';
wrapper.style.pointerEvents = 'auto';
} else {
wrapper.style.opacity = '0.5';
wrapper.style.pointerEvents = 'none';
}
validateBackpacks();
});
});
// Validation Logic (Copied from add_packing_list.php)
function validateBackpacks() {
const selected = [];
let hasDuplicate = false;
const inputs = document.querySelectorAll('input[name^="backpacks["]');
inputs.forEach(input => {
const userIdMatch = input.name.match(/backpacks\[(\d+)\]/);
if (userIdMatch) {
const userId = userIdMatch[1];
const partCheck = document.getElementById('part_' + userId);
if (partCheck && partCheck.checked) {
const val = parseInt(input.value);
if (val > 0) {
if (selected.includes(val)) {
hasDuplicate = true;
}
selected.push(val);
}
}
}
});
if (hasDuplicate) {
warning.classList.remove('d-none');
submitBtn.disabled = true;
} else {
warning.classList.add('d-none');
submitBtn.disabled = false;
}
}
document.body.addEventListener('hidden.bs.modal', validateBackpacks);
validateBackpacks();
});
</script>
<?php require_once 'footer.php'; ?>

View File

@@ -69,25 +69,32 @@ $stmt_all_articles->execute();
$all_articles_raw = $stmt_all_articles->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_all_articles->close();
$sql_items = "SELECT pli.id, pli.article_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) as name, a.weight_grams, a.product_designation, a.consumable, m.name as manufacturer_name
$sql_items = "SELECT pli.id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id, pli.backpack_id, pli.backpack_compartment_id,
COALESCE(
a.name,
bc.name,
CASE WHEN b.name IS NOT NULL THEN CONCAT('Rucksack: ', b.name) ELSE NULL END,
pli.name
) as name,
a.weight_grams, a.product_designation, a.consumable, m.name as manufacturer_name
FROM packing_list_items pli
LEFT JOIN articles a ON pli.article_id = a.id
LEFT JOIN manufacturers m ON a.manufacturer_id = m.id
LEFT JOIN backpacks b ON pli.backpack_id = b.id
LEFT JOIN backpack_compartments bc ON pli.backpack_compartment_id = bc.id
WHERE pli.packing_list_id = ?
AND (pli.carrier_user_id IN ($placeholders) OR pli.carrier_user_id IS NULL)
ORDER BY pli.order_index ASC";
$stmt_items = $conn->prepare($sql_items);
$params_items = array_merge([$packing_list_id], $household_member_ids);
$types_items = 'i' . $types;
$stmt_items->bind_param($types_items, ...$params_items);
$stmt_items->bind_param("i", $packing_list_id);
$stmt_items->execute();
$packed_items_raw = $stmt_items->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_items->close();
$carriers_data = [];
$stmt_carriers = $conn->prepare("SELECT id, username FROM users WHERE id IN ($placeholders)");
$stmt_carriers->bind_param($types, ...$household_member_ids);
// KORREKTUR: Nur Träger anzeigen, die dieser Liste zugewiesen sind
$stmt_carriers = $conn->prepare("SELECT u.id, u.username FROM users u JOIN packing_list_carriers plc ON u.id = plc.user_id WHERE plc.packing_list_id = ? ORDER BY u.username");
$stmt_carriers->bind_param("i", $packing_list_id);
$stmt_carriers->execute();
$carriers_result = $stmt_carriers->get_result();
while ($row = $carriers_result->fetch_assoc()) { $carriers_data[] = $row; }
@@ -118,8 +125,8 @@ $conn->close();
<h5 class="mb-0">Verfügbare Artikel</h5>
<div class="row g-2 mt-2">
<div class="col-12"><input type="text" id="filter-text" class="form-control form-control-sm" placeholder="Artikel, Hersteller, Modell suchen..."></div>
<div class="col-md-6"><select id="filter-category" class="form-select form-select-sm"><option value="">Alle Kategorien</option><?php foreach($categories as $c) echo '<option>'.htmlspecialchars($c).'</option>'; ?></select></div>
<div class="col-md-6"><select id="filter-manufacturer" class="form-select form-select-sm"><option value="">Alle Hersteller</option><?php foreach($manufacturers as $m) echo '<option>'.htmlspecialchars($m).'</option>'; ?></select></div>
<div class="col-md-6"><select id="filter-category" class="form-select form-select-sm"><option value="">-- Alle Kategorien --</option><?php foreach($categories as $c) echo '<option>'.htmlspecialchars($c).'</option>'; ?></select></div>
<div class="col-md-6"><select id="filter-manufacturer" class="form-select form-select-sm"><option value="">-- Alle Hersteller --</option><?php foreach($manufacturers as $m) echo '<option>'.htmlspecialchars($m).'</option>'; ?></select></div>
</div>
</div>
<div class="pane-content" id="available-items-list"></div>
@@ -220,11 +227,13 @@ $conn->close();
let childrenModal = null;
document.addEventListener('DOMContentLoaded', () => {
// Initialize Tom Select for filters
// Initialize Tom Select for filters with allowEmptyOption
const tsOptions = {
create: false,
sortField: { field: "text", direction: "asc" },
allowEmptyOption: true,
onChange: function(value) {
// Propagate change
this.input.dispatchEvent(new Event('change'));
}
};
@@ -492,19 +501,6 @@ $conn->close();
const pliId = child.dataset.itemId;
const articleId = child.dataset.articleId;
// Check for container data
let bpId = null;
let bpCompId = null;
if (child.querySelector('.backpack-root-item')) {
// Assuming we store this in dataset or infer from something?
// Actually, the sync relies on existing items usually. But for structure,
// we need to persist these IDs if they exist on the element.
// Since createPackedItemDOM doesn't attach them to dataset, let's fix that or rely on existing data logic?
// Better: attach to dataset in createPackedItemDOM
}
// Correction: The frontend re-sends the whole tree. If we don't send bpId, it might be lost if backend just upserts.
// However, backend 'sync_list' usually recreates or updates.
// Let's add data attributes to the DOM first.
payload.list.push({
pli_id: pliId,
@@ -530,9 +526,8 @@ $conn->close();
// Save scroll positions
const scrollPos = {
avail: document.getElementById('available-items-list').scrollTop,
carrier: document.getElementById('carriers-container').scrollTop // Note: carriers-container itself might not scroll, but the panes do
carrier: document.getElementById('carriers-container').scrollTop
};
// The actual scrolling element is .pane-content
const panes = document.querySelectorAll('.pane-content');
const scrollMap = Array.from(panes).map(p => p.scrollTop);

View File

@@ -1,6 +1,6 @@
<?php
// packing_list_detail.php - Detailansicht einer Packliste
// FINALE, STABILE VERSION: Mit modernem Tree-View, Toggle-Funktion und externem CSS.
// FINALE, STABILE VERSION: Mit modernem Tree-View, Toggle-Funktion, Grünen Charts & Träger-Modal
if (session_status() == PHP_SESSION_NONE) {
session_start();
@@ -21,6 +21,9 @@ $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.");
}
@@ -57,22 +60,23 @@ $stmt_list_owner->close();
$page_title = "Packliste: " . htmlspecialchars($packing_list['name']);
// Robust SQL: Fetches names from Backpacks/Compartments if article is missing
// FIX: Include backpack own weight in weight_grams calculation
// 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,
c.name AS category_name,
COALESCE(c.name, c_bp.name, 'Sonstiges') AS category_name,
m.name AS manufacturer_name,
u.username AS carrier_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 = ?
@@ -87,9 +91,6 @@ $items_by_id = [];
$items_by_parent = [];
while ($row = $result->fetch_assoc()) {
// Fix Names for Display: Removed redundant prefix logic
// if ($row['backpack_id']) $row['article_name'] = "Rucksack: " . $row['article_name'];
$items_by_id[$row['id']] = $row;
$parent_id = $row['parent_packing_list_item_id'] ?: 0;
if (!isset($items_by_parent[$parent_id])) {
@@ -102,6 +103,9 @@ while ($row = $result->fetch_assoc()) {
$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;
@@ -115,13 +119,33 @@ while ($row = $result->fetch_assoc()) {
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 for recursive counting
// Helper functions (same as before)
function get_recursive_quantity($parent_id, $items_by_parent) {
$count = 0;
if (isset($items_by_parent[$parent_id])) {
@@ -133,41 +157,41 @@ function get_recursive_quantity($parent_id, $items_by_parent) {
return $count;
}
// Helper for recursive weight calculation
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) {
// Child weight
$weight += ($child['quantity'] * $child['weight_grams']);
// Plus its descendants
$weight += get_recursive_weight($child['id'], $items_by_parent);
}
}
return $weight;
}
// Recursive Rendering
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']);
// Visual Styles
$bg_class = "";
$text_class = "";
$icon = "";
if ($is_backpack) {
$bg_class = "table-success"; // Greenish for Backpack
$bg_class = "table-success";
$text_class = "fw-bold text-uppercase";
$icon = '<i class="fas fa-hiking me-2 text-success"></i>';
} elseif ($is_compartment) {
$bg_class = "table-light"; // Light gray for Compartment
$bg_class = "table-light";
$text_class = "fw-bold fst-italic text-muted";
$icon = '<i class="fas fa-folder-open me-2 text-warning"></i>';
// Check if linked article (image present)
if (!empty($item['image_url'])) {
$img_src = htmlspecialchars($item['image_url']);
$icon = '<img src="' . $img_src . '" class="item-image me-2 article-image-trigger" data-preview-url="' . $img_src . '">';
} else {
$icon = '<i class="fas fa-folder-open me-2 text-warning"></i>';
}
} else {
// Standard Item
$img_src = !empty($item['image_url']) ? htmlspecialchars($item['image_url']) : 'assets/images/keinbild.png';
$icon = '<img src="' . $img_src . '" class="item-image me-2 article-image-trigger" data-preview-url="' . $img_src . '">';
}
@@ -175,16 +199,12 @@ function render_item_row($item, $level, $items_by_parent) {
$indent_px = $level * 25;
$weight_display = $item['weight_grams'] > 0 ? number_format($item['weight_grams'], 0, ',', '.') . ' g' : '-';
// Calculate Total Weight display logic
$total_weight_val = 0;
if ($is_backpack || $is_compartment) {
// For containers: Recursive weight of children
// For backpacks specifically: Add own weight + children
$children_weight = get_recursive_weight($item['id'], $items_by_parent);
$own_weight = $item['quantity'] * $item['weight_grams']; // Usually 1 * empty_weight
$own_weight = $item['quantity'] * $item['weight_grams'];
$total_weight_val = $own_weight + $children_weight;
} else {
// Standard items
$total_weight_val = $item['weight_grams'] * $item['quantity'];
}
@@ -192,13 +212,10 @@ function render_item_row($item, $level, $items_by_parent) {
echo '<tr class="' . $bg_class . '" data-id="' . $item['id'] . '" data-parent-id="' . ($item['parent_packing_list_item_id'] ?: 0) . '">';
// Name Column with Indentation
echo '<td>';
echo '<div style="padding-left: ' . $indent_px . 'px; display: flex; align-items: center;">';
// Tree Toggle or Spacer
if ($has_children) {
// Explicitly style the button
echo '<button type="button" class="btn btn-sm p-0 me-2 border-0 bg-transparent text-primary toggle-tree-btn" data-target-id="' . $item['id'] . '" style="width:20px; cursor:pointer;"><i class="fas fa-chevron-down"></i></button>';
} else {
echo '<span style="width: 20px; display: inline-block; margin-right: 0.5rem;"></span>';
@@ -209,26 +226,20 @@ function render_item_row($item, $level, $items_by_parent) {
echo '</div>';
echo '</td>';
// Other Columns
echo '<td class="text-center">' . ($item['consumable'] ? '<i class="fas fa-cookie-bite text-warning" title="Verbrauch"></i>' : '') . '</td>';
echo '<td>' . htmlspecialchars($item['manufacturer_name'] ?: '') . '</td>';
echo '<td>' . htmlspecialchars($item['product_designation'] ?: '') . '</td>';
echo '<td>' . htmlspecialchars($item['category_name'] ?: '') . '</td>';
// Quantity / Child Count Badge
echo '<td class="text-center">';
if ($is_backpack) {
// Rucksack: Keine Summe anzeigen (User Wunsch)
echo '';
} elseif ($is_compartment) {
// Fächer: Rekursive Summe aller enthaltenen Artikel
$total_items = get_recursive_quantity($item['id'], $items_by_parent);
if ($total_items > 0) {
// Grün, aber gleiche Form wie Standard (kein rounded-pill)
echo '<span class="badge bg-success text-white border border-success" title="Enthält ' . $total_items . ' Artikel">' . $total_items . '</span>';
}
} else {
// Standard Artikel Menge
echo '<span class="badge bg-white text-dark border">' . $item['quantity'] . 'x</span>';
}
echo '</td>';
@@ -244,15 +255,6 @@ function render_item_row($item, $level, $items_by_parent) {
}
}
}
function render_print_table_rows($items, $level = 0) {
foreach($items as $item) {
// Simulate structure for print function which might expect different array format?
// Actually, this function is called below with items from $sorted_items_by_carrier which likely won't work
// because we rewrote the main fetching logic to flattened array $items_by_parent.
// We need to adapt the print view section below to use $items_by_parent logic.
}
}
?>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
@@ -294,7 +296,6 @@ function render_print_table_rows($items, $level = 0) {
<tr><td colspan="8" class="text-center p-4 text-muted">Liste ist leer.</td></tr>
<?php else: ?>
<?php
// Group roots by carrier
$items_by_carrier = [];
foreach ($items_by_parent[0] as $root) {
$c = $root['carrier_name'] ?: 'Sonstiges';
@@ -303,7 +304,13 @@ function render_print_table_rows($items, $level = 0) {
foreach ($items_by_carrier as $carrier => $roots): ?>
<tr class="table-secondary border-bottom border-white">
<td colspan="8" class="fw-bold text-uppercase ps-3 py-2"><i class="fas fa-user-circle me-2"></i><?php echo htmlspecialchars($carrier); ?></td>
<td colspan="8" class="fw-bold text-uppercase ps-3 py-2">
<i class="fas fa-user-circle me-2"></i>
<a href="#" class="text-dark text-decoration-none carrier-stats-link" data-carrier="<?php echo htmlspecialchars($carrier); ?>">
<?php echo htmlspecialchars($carrier); ?>
<small class="text-muted ms-2 font-weight-normal">(Klicken für Statistik)</small>
</a>
</td>
</tr>
<?php foreach ($roots as $root_item):
render_item_row($root_item, 0, $items_by_parent);
@@ -341,7 +348,6 @@ function render_print_table_rows($items, $level = 0) {
<div class="col-6"><h6 class="text-center small">nach Träger</h6><div class="chart-container"><canvas id="carrierWeightChart"></canvas></div></div>
</div>
<!-- Re-added Category Weight Table -->
<div class="stats-table-container mt-4">
<h6>Gewicht nach Kategorie</h6>
<div class="table-responsive">
@@ -366,39 +372,96 @@ function render_print_table_rows($items, $level = 0) {
</div>
</div>
<!-- Modal for Carrier Statistics -->
<div class="modal fade" id="carrierStatsModal" tabindex="-1" aria-labelledby="carrierStatsModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="carrierStatsModalLabel">Statistik für Träger</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body">
<div id="carrier-stats-content">
<!-- Populated via JS -->
</div>
</div>
</div>
</div>
</div>
<div id="image-preview-tooltip"></div>
<script>
// Pass PHP Data to JS
const carrierStatsData = <?php echo json_encode($carrier_stats_details); ?>;
document.addEventListener('DOMContentLoaded', function() {
// Tooltip logic
// Tooltip logic with EVENT DELEGATION
const tooltip = document.getElementById('image-preview-tooltip');
if (tooltip) {
document.querySelectorAll('.article-image-trigger').forEach(trigger => {
trigger.addEventListener('mouseover', e => {
document.body.addEventListener('mouseover', function(e) {
if (e.target.classList.contains('article-image-trigger')) {
const url = e.target.getAttribute('data-preview-url');
if (url && !url.endsWith('assets/images/keinbild.png')) {
tooltip.style.backgroundImage = `url('${url}')`;
tooltip.style.display = 'block';
}
});
trigger.addEventListener('mousemove', e => { tooltip.style.left = e.pageX + 15 + 'px'; tooltip.style.top = e.pageY + 15 + 'px'; });
trigger.addEventListener('mouseout', () => { tooltip.style.display = 'none'; });
}
});
document.body.addEventListener('mousemove', function(e) {
if (tooltip.style.display === 'block') {
tooltip.style.left = e.pageX + 15 + 'px';
tooltip.style.top = e.pageY + 15 + 'px';
}
});
document.body.addEventListener('mouseout', function(e) {
if (e.target.classList.contains('article-image-trigger')) {
tooltip.style.display = 'none';
}
});
}
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) });
// Chart logic
// Chart logic (PURE GREEN TONES - High Contrast)
Chart.register(ChartDataLabels);
const highContrastColors = ['#00429d', '#4771b2', '#73a2c6', '#a4d3d9', '#deeedb', '#ffc993', '#f48f65', '#d45039', '#930000'];
const greenColors = [
'#2e7d32', // Dark Forest Green
'#4caf50', // Standard Green
'#81c784', // Light Pastel Green
'#a5d6a7', // Very Light Green
'#1b5e20', // Deep Dark Green
'#66bb6a', // Medium Green
'#00e676', // Neon Green (Accent)
'#c8e6c9', // White-Green
'#00c853', // Vibrant Green
'#69f0ae', // Soft Neon
'#388e3c' // Solid Green
];
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: 20 // Add padding to prevent cutting off hover
},
plugins: {
legend: { display: false },
datalabels: { display: false }
}
};
const catCtx = document.getElementById('categoryWeightChart');
if (catCtx) {
new Chart(catCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(array_keys($weight_by_category)); ?>, datasets: [{ data: <?php echo json_encode(array_values($weight_by_category)); ?>, backgroundColor: highContrastColors, borderWidth: 2, hoverOffset: 15 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } } } });
new Chart(catCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(array_keys($weight_by_category)); ?>, datasets: [{ data: <?php echo json_encode(array_values($weight_by_category)); ?>, backgroundColor: greenColors, borderWidth: 0, hoverOffset: 20 }] }, options: chartOptions });
}
const carrierCtx = document.getElementById('carrierWeightChart');
if (carrierCtx) {
new Chart(carrierCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(array_keys($weight_by_carrier)); ?>, datasets: [{ data: <?php echo json_encode(array_values($weight_by_carrier)); ?>, backgroundColor: highContrastColors, borderWidth: 2, hoverOffset: 15 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } } } });
new Chart(carrierCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(array_keys($weight_by_carrier)); ?>, datasets: [{ data: <?php echo json_encode(array_values($weight_by_carrier)); ?>, backgroundColor: greenColors, borderWidth: 0, hoverOffset: 20 }] }, options: chartOptions });
}
// Collapsible Tree Logic
@@ -433,6 +496,59 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
}
// Carrier Stats Modal Logic
const statsModal = new bootstrap.Modal(document.getElementById('carrierStatsModal'));
document.querySelectorAll('.carrier-stats-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const carrierName = this.getAttribute('data-carrier');
const data = carrierStatsData[carrierName];
if (data) {
document.getElementById('carrierStatsModalLabel').innerText = 'Statistik für ' + carrierName;
// Format Numbers
const fmt = (n) => new Intl.NumberFormat('de-DE').format(n) + ' g';
let catRows = '';
// Sort categories by weight desc
const sortedCats = Object.entries(data.categories).sort((a, b) => b[1] - a[1]);
sortedCats.forEach(([cat, w]) => {
catRows += `<tr><td>${cat}</td><td class="text-end">${fmt(w)}</td></tr>`;
});
const content = `
<div class="row mb-3">
<div class="col-6">
<div class="card text-center bg-light h-100">
<div class="card-body py-2">
<small class="text-muted">Gesamtgewicht</small>
<h5 class="card-title mb-0 text-primary">${fmt(data.total_weight)}</h5>
</div>
</div>
</div>
<div class="col-6">
<div class="card text-center bg-light h-100">
<div class="card-body py-2">
<small class="text-muted">Basisgewicht (o. Verb.)</small>
<h5 class="card-title mb-0 text-success">${fmt(data.base_weight)}</h5>
</div>
</div>
</div>
</div>
<h6 class="border-bottom pb-2">Kategorien</h6>
<table class="table table-sm table-striped">
<tbody>${catRows}</tbody>
</table>
`;
document.getElementById('carrier-stats-content').innerHTML = content;
statsModal.show();
}
});
});
});
</script>

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,18 @@ require_once 'db_connect.php';
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
// AUTO-MIGRATION: Check columns to prevent SQL errors
$check_col = $conn->query("SHOW COLUMNS FROM packing_lists LIKE 'is_template'");
if ($check_col && $check_col->num_rows == 0) {
// Add column if missing
$conn->query("ALTER TABLE packing_lists ADD COLUMN is_template TINYINT(1) NOT NULL DEFAULT 0 AFTER share_token");
}
$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 +39,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 +51,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 +73,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 +133,46 @@ $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">
</form>
<?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="Inhalt bearbeiten"><i class="fas fa-boxes"></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="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): ?>
<!-- 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 +184,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;
?>