Compare commits
25 Commits
a64a09bc8f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7744168d0d | ||
|
|
7bdc421d90 | ||
|
|
0753e2c4b9 | ||
|
|
a19cfa98f0 | ||
| 8c50910363 | |||
|
|
59104fb3bd | ||
|
|
b9446b01a3 | ||
|
|
a4b74ab480 | ||
|
|
991f264e29 | ||
|
|
80ede8e3a6 | ||
|
|
70eef4cd82 | ||
|
|
967bae965a | ||
|
|
f9df3087a7 | ||
|
|
dd1cbcb2a6 | ||
|
|
f1d9634dba | ||
|
|
36660fdd51 | ||
| 0c4cc705a6 | |||
| df2a98ae28 | |||
|
|
f7733dfce5 | ||
|
|
620c4127c5 | ||
|
|
c1f4ec1de7 | ||
|
|
1c13862640 | ||
|
|
aaf31a3b4b | ||
|
|
47e51ce60f | ||
|
|
0392c69019 |
@@ -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
|
||||
51
README.md
51
README.md
@@ -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.
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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'; ?>
|
||||
@@ -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>';
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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'; ?>
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
101
src/packing_list_utils.php
Normal 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();
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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
58
src/save_as_template.php
Normal 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;
|
||||
?>
|
||||
Reference in New Issue
Block a user