Compare commits

..

16 Commits

Author SHA1 Message Date
Gemini Agent
7744168d0d Fix: Restore 'Edit Details' button for standard packing lists
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 23s
2025-12-08 21:44:43 +00:00
Gemini Agent
7bdc421d90 Fix: Correct display of new templates and limit carriers to assigned users
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 19s
2025-12-08 21:40:13 +00:00
Gemini Agent
0753e2c4b9 Fix: Template improvements
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 38s
- api_packing_list_handler.php: Use robust LEFT JOINs to fetch backpack/compartment names, fixing '0' display issue for templates.
- packing_lists.php: Add 'Edit Details' button for templates.
- edit_packing_list_details.php: Allow sharing templates with household (checkbox) and adjust title for templates.
2025-12-08 21:35:08 +00:00
Gemini Agent
a19cfa98f0 Fix: Add Auto-Migration for 'is_template' column in packing_lists.php to prevent SQL errors on deployment
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 17s
2025-12-08 21:28:34 +00:00
8c50910363 .gitea/workflows/build-push.yaml aktualisiert
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 25s
2025-12-08 21:20:01 +00:00
Gemini Agent
59104fb3bd Docs: Update README with Template feature details
Some checks failed
Docker Build & Push / build-and-push (push) Failing after 4s
2025-12-08 21:12:08 +00:00
Gemini Agent
b9446b01a3 Feature: Packlisten-Templates hinzugefügt
Some checks failed
Docker Build & Push / build-and-push (push) Failing after 15s
- DB-Schema erweitert (is_template Spalte in packing_lists)
- packing_lists.php: Tabs für Listen/Vorlagen und Filterung
- save_as_template.php: Funktion zum Speichern einer Liste als Vorlage
- add_packing_list.php: Neue Liste aus Vorlage erstellen
- packing_list_utils.php: Zentrale Kopier-Logik (inkl. Träger & Hierarchien)
- edit_packing_list_details.php: Navigation angepasst
2025-12-08 21:11:45 +00:00
Gemini Agent
a4b74ab480 Docs & Fix: packliste.sql aktualisiert, Auto-Migration und Changelog ergänzt.
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 14s
2025-12-07 08:06:24 +00:00
Gemini Agent
991f264e29 Fix: Rucksack-Kategorie (DB/UI/Stats) und Tooltip-Zuverlässigkeit
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 28s
2025-12-07 08:04:32 +00:00
Gemini Agent
80ede8e3a6 Fix: Zusatztaschen-Gewicht und Bilder in Packlisten
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 16s
- backpack_utils.php: Setze article_id beim Sync von Zusatztaschen korrekt.
- packing_list_detail.php: Zeige Artikelbild statt Ordner-Icon bei Zusatztaschen.
2025-12-07 07:57:05 +00:00
Gemini Agent
70eef4cd82 Fix: Gewichtsberechnung Zusatztaschen, UI-Konsistenz bei Editieren, Icons
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 23s
2025-12-07 07:52:03 +00:00
Gemini Agent
967bae965a Fix: TomSelect Dropdown-Anzeige (Z-Index, Images) in Rucksack-Bearbeitung
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 16s
2025-12-07 07:46:17 +00:00
Gemini Agent
f9df3087a7 Fix: Auto-Migration in backpacks.php, um SQL-Fehler bei fehlenden Spalten zu verhindern.
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 20s
2025-12-07 07:40:07 +00:00
Gemini Agent
dd1cbcb2a6 Feature: Modulare Rucksäcke (Zusatztaschen)
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 26s
- DB-Erweiterung für linked_article_id in backpack_compartments.
- UI in edit_backpack.php zum Verknüpfen von Artikeln als Fächer.
- Sync-Logik in backpack_utils.php übernimmt Artikel-Daten.
- Gewichts-Kalkulation in backpacks.php berücksichtigt Zusatztaschen.
- Changelog aktualisiert.
2025-12-07 07:36:34 +00:00
Gemini Agent
f1d9634dba Merge branch 'master' of https://git.klenzel.net/admin/packliste
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 22s
2025-12-06 17:28:12 +00:00
Gemini Agent
36660fdd51 Feature: Bildbibliothek als Modal in Artikel-Erstellung (bis zu 500 Bilder) 2025-12-06 17:28:07 +00:00
16 changed files with 1039 additions and 412 deletions

View File

@@ -6,7 +6,7 @@ on:
branches:
- main
- master
jobs:
build-and-push:
runs-on: ubuntu-latest

View File

@@ -127,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)
@@ -165,6 +174,13 @@ Das Projekt basiert auf bewährten Web-Standards:
## 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.
@@ -180,3 +196,13 @@ Das Projekt basiert auf bewährten Web-Standards:
* **Details:**
* Design-Update: Diagramme verwenden nun kontrastreiche Grüntöne passend zum Thema (ohne weiße Rahmen).
* Statistik: Klickbare Trägernamen öffnen ein Modal mit detaillierten Gewichtsstatistiken pro Kategorie.
* **Modulare Rucksack-Erweiterung:**
* Fächer können nun direkt mit Artikeln verknüpft werden (z.B. "Hüftgurttasche").
* Diese "Zusatztaschen" werden in der Packliste automatisch als Container angelegt.
* Das Gewicht der Zusatztaschen wird automatisch zum Rucksack-Gesamtgewicht addiert.
* **Rucksack-Kategorie:** Rucksäcke können nun einer Kategorie (z.B. "Transport") zugewiesen werden, damit sie in der Statistik korrekt auftauchen.
* **Fixes:**
* Datenbank-Selbstheilung (`Auto-Migration`) in `backpacks.php` und `edit_backpack.php` integriert.
* `packliste.sql` aktualisiert.
* Anzeige-Probleme bei Dropdowns behoben.
* SQL-Fehler in der Statistik-Berechnung korrigiert.

View File

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

View File

@@ -1,6 +1,6 @@
<?php
// add_article.php - Formular zum Hinzufügen eines neuen Artikels
// FINALE, VOLLSTÄNDIGE VERSION mit Bilder-Auswahl
// FINALE, VOLLSTÄNDIGE VERSION mit Bilder-Bibliothek Modal
$page_title = "Neuen Artikel hinzufügen";
@@ -74,11 +74,11 @@ $stmt_parent_articles->execute();
$parent_articles = $stmt_parent_articles->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_parent_articles->close();
// Lade letzte Bilder (für Auswahl)
$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 24");
// 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();
$recent_images = $stmt_imgs->get_result()->fetch_all(MYSQLI_ASSOC);
$all_images = $stmt_imgs->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_imgs->close();
// Strukturierung Lagerorte
@@ -266,19 +266,12 @@ $conn->close();
<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: Existing Images Selection -->
<?php if(!empty($recent_images)): ?>
<!-- New: Button to open modal -->
<?php if(!empty($all_images)): ?>
<div class="mb-3">
<label class="form-label">Aus vorhandenen Bildern wählen:</label>
<div class="d-flex flex-wrap gap-2 p-2 border rounded bg-light" style="max-height: 150px; overflow-y: auto;">
<?php foreach($recent_images as $img): ?>
<img src="<?php echo htmlspecialchars($img['image_url']); ?>"
class="existing-image-option rounded border"
style="width: 50px; height: 50px; object-fit: cover; cursor: pointer;"
data-url="<?php echo htmlspecialchars($img['image_url']); ?>"
title="Klicken zum Auswählen">
<?php endforeach; ?>
</div>
<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; ?>
@@ -297,7 +290,80 @@ $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);
@@ -305,19 +371,14 @@ document.addEventListener('DOMContentLoaded', function() {
if (!select || !container) return;
function toggle() {
// Must check TomSelect wrapper input if handled, but the value propagates
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 = ''; }
}
// Native change (for backup)
select.addEventListener('change', toggle);
// Initial
toggle();
// Return the toggle function so TomSelect can call it
return toggle;
}
const toggleMan = setupAddNewOption('manufacturer_id', 'new_manufacturer_container');
@@ -329,16 +390,12 @@ document.addEventListener('DOMContentLoaded', function() {
const pasteArea = document.getElementById('pasteArea');
const pastedImageDataInput = document.getElementById('pasted_image_data');
const selectedExistingInput = document.getElementById('selected_existing_image');
const existingImages = document.querySelectorAll('.existing-image-option');
function clearOtherImageInputs(source) {
if (source !== 'file') imageFileInput.value = '';
if (source !== 'url') imageUrlInput.value = '';
if (source !== 'paste') pastedImageDataInput.value = '';
if (source !== 'existing') selectedExistingInput.value = '';
// Reset styles
existingImages.forEach(img => img.classList.remove('border-primary', 'border-3'));
}
if(imageFileInput) {
@@ -352,17 +409,6 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Existing Image Click Handler
existingImages.forEach(img => {
img.addEventListener('click', function() {
const url = this.getAttribute('data-url');
imagePreview.src = url;
clearOtherImageInputs('existing');
selectedExistingInput.value = url;
this.classList.add('border-primary', 'border-3');
});
});
if (pasteArea) {
pasteArea.addEventListener('paste', function(e) {
e.preventDefault();
@@ -383,7 +429,6 @@ 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'); });
// Click to focus
pasteArea.setAttribute('tabindex', '0');
pasteArea.addEventListener('click', () => pasteArea.focus());
}
@@ -399,7 +444,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// Initialize Tom Select for searchable dropdowns
// Initialize Tom Select
const tsOptionsBase = {
create: false,
sortField: { field: "text", direction: "asc" }

View File

@@ -13,6 +13,7 @@ if (!isset($_SESSION['user_id'])) {
require_once 'db_connect.php';
require_once 'household_actions.php';
require_once 'backpack_utils.php';
require_once 'packing_list_utils.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
@@ -26,6 +27,23 @@ $stmt_household->execute();
$household_id_for_user = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
// Fetch available templates
$templates = [];
$sql_templates = "SELECT id, name FROM packing_lists WHERE is_template = 1 AND (user_id = ? OR household_id = ?)";
$stmt_templates = $conn->prepare($sql_templates);
// Falls household_id NULL ist, binden wir trotzdem User ID als Dummy oder handeln es anders.
// Einfacher: Wir nutzen current_user_id zweimal, wenn household NULL, oder Logik im SQL.
// Da household_id INT ist, kann es NULL sein.
// Besser:
$h_id = $household_id_for_user ?: 0; // 0 matches nothing usually, safe
$stmt_templates->bind_param("ii", $current_user_id, $h_id);
$stmt_templates->execute();
$res_templates = $stmt_templates->get_result();
while ($row = $res_templates->fetch_assoc()) {
$templates[] = $row;
}
$stmt_templates->close();
// Fetch Users for Backpack Assignment UI
$available_users = [];
if ($household_id_for_user) {
@@ -45,21 +63,25 @@ $stmt_u->close();
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$name = trim($_POST['name']);
$description = trim($_POST['description']);
$template_id = isset($_POST['template_id']) ? intval($_POST['template_id']) : 0;
$household_id = isset($_POST['is_household_list']) && $household_id_for_user ? $household_id_for_user : NULL;
// Server-Side Validation for duplicate backpacks
$selected_backpacks = [];
// Server-Side Validation for duplicate backpacks (ONLY if no template is selected)
$has_duplicate_backpacks = false;
if (isset($_POST['participate']) && is_array($_POST['participate'])) {
foreach ($_POST['participate'] as $uid => $val) {
if ($val) {
$bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0;
if ($bid > 0) {
if (in_array($bid, $selected_backpacks)) {
$has_duplicate_backpacks = true;
break;
if ($template_id == 0) {
$selected_backpacks = [];
if (isset($_POST['participate']) && is_array($_POST['participate'])) {
foreach ($_POST['participate'] as $uid => $val) {
if ($val) {
$bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0;
if ($bid > 0) {
if (in_array($bid, $selected_backpacks)) {
$has_duplicate_backpacks = true;
break;
}
$selected_backpacks[] = $bid;
}
$selected_backpacks[] = $bid;
}
}
}
@@ -70,7 +92,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
} elseif ($has_duplicate_backpacks) {
$message = '<div class="alert alert-danger" role="alert">Fehler: Ein Rucksack kann nicht mehreren Personen zugewiesen werden. Bitte korrigieren Sie die Auswahl.</div>';
} else {
$stmt = $conn->prepare("INSERT INTO packing_lists (user_id, household_id, name, description) VALUES (?, ?, ?, ?)");
$stmt = $conn->prepare("INSERT INTO packing_lists (user_id, household_id, name, description, is_template) VALUES (?, ?, ?, ?, 0)");
if ($stmt === false) {
$message .= '<div class="alert alert-danger" role="alert">SQL Prepare-Fehler: ' . $conn->error . '</div>';
} else {
@@ -78,23 +100,37 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
if ($stmt->execute()) {
$new_list_id = $conn->insert_id;
if (isset($_POST['participate']) && is_array($_POST['participate'])) {
foreach ($_POST['participate'] as $uid => $val) {
$uid = intval($uid);
if ($val) {
$bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0;
$bid_to_insert = ($bid > 0) ? $bid : NULL;
$stmt_in = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)");
$stmt_in->bind_param("iii", $new_list_id, $uid, $bid_to_insert);
$stmt_in->execute();
$stmt_in->close();
if ($bid > 0) {
sync_backpack_items($conn, $new_list_id, $uid, $bid);
if ($template_id > 0) {
// Option A: Aus Template erstellen
try {
copyPackingListContent($template_id, $new_list_id, $conn);
$_SESSION['message'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich aus Vorlage erstellt!</div>';
} catch (Exception $e) {
// Rollback oder Warnung? Wir lassen die Liste da, aber warnen.
$_SESSION['message'] = '<div class="alert alert-warning" role="alert">Liste erstellt, aber Fehler beim Kopieren der Vorlage: ' . $e->getMessage() . '</div>';
}
} else {
// Option B: Leere Liste mit manuellen Trägern
if (isset($_POST['participate']) && is_array($_POST['participate'])) {
foreach ($_POST['participate'] as $uid => $val) {
$uid = intval($uid);
if ($val) {
$bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0;
$bid_to_insert = ($bid > 0) ? $bid : NULL;
$stmt_in = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)");
$stmt_in->bind_param("iii", $new_list_id, $uid, $bid_to_insert);
$stmt_in->execute();
$stmt_in->close();
if ($bid > 0) {
sync_backpack_items($conn, $new_list_id, $uid, $bid);
}
}
}
}
$_SESSION['message'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich erstellt!</div>';
}
if ($household_id_for_user) {
@@ -102,7 +138,6 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
log_household_action($conn, $household_id_for_user, $current_user_id, $log_message);
}
$_SESSION['message'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich erstellt!</div>';
header("Location: manage_packing_list_items.php?id=" . $new_list_id);
exit;
} else {
@@ -131,6 +166,21 @@ require_once 'header.php';
<label for="name" class="form-label"><i class="fas fa-file-signature me-2 text-muted"></i>Name der Packliste</label>
<input type="text" class="form-control" id="name" value="<?php echo htmlspecialchars($name); ?>" name="name" required>
</div>
<!-- Template Selection -->
<div class="mb-3">
<label for="template_id" class="form-label"><i class="fas fa-copy me-2 text-muted"></i>Vorlage verwenden (Optional)</label>
<select class="form-select" id="template_id" name="template_id">
<option value="0" selected>Keine Vorlage (Leere Liste starten)</option>
<?php foreach ($templates as $tpl): ?>
<option value="<?php echo $tpl['id']; ?>">
<?php echo htmlspecialchars($tpl['name']); ?>
</option>
<?php endforeach; ?>
</select>
<div class="form-text">Wenn du eine Vorlage wählst, werden Artikel, Träger und Rucksäcke daraus übernommen.</div>
</div>
<div class="mb-3">
<label for="description" class="form-label"><i class="fas fa-align-left me-2 text-muted"></i>Beschreibung (optional)</label>
<textarea class="form-control" id="description" name="description" rows="3"><?php echo htmlspecialchars($description); ?></textarea>
@@ -144,43 +194,49 @@ require_once 'header.php';
</div>
<?php endif; ?>
</div>
<div class="col-md-5">
<h5 class="mb-3"><i class="fas fa-users me-2 text-muted"></i>Teilnehmer & Rucksäcke</h5>
<div class="card bg-light border-0">
<div class="card-body">
<p class="small text-muted">Wähle aus, wer mitkommt und wer welchen Rucksack trägt.</p>
<div id="backpack-warning" class="alert alert-warning d-none small p-2 mb-2">
<i class="fas fa-exclamation-triangle me-1"></i> Achtung: Ein Rucksack wurde mehrfach ausgewählt!
</div>
<?php
$user_backpacks_json = [];
$all_assigned_backpack_ids = [];
foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $household_id_for_user);
$current_bp_id = 0;
$user_backpacks_json[$user['id']] = [
'current_id' => $current_bp_id,
'backpacks' => $user_backpacks
];
?>
<div class="mb-3 pb-3 border-bottom">
<div class="form-check mb-2">
<input class="form-check-input participation-check" type="checkbox" name="participate[<?php echo $user['id']; ?>]" value="1" checked id="part_<?php echo $user['id']; ?>">
<label class="form-check-label fw-bold" for="part_<?php echo $user['id']; ?>">
<?php echo htmlspecialchars($user['username']); ?>
</label>
</div>
<div class="ms-4">
<!-- Pass class for JS validation -->
<?php echo render_backpack_card_selector($user, $current_bp_id, $user_backpacks); ?>
</div>
<div id="participants-section">
<div class="card bg-light border-0">
<div class="card-body">
<p class="small text-muted">Wähle aus, wer mitkommt und wer welchen Rucksack trägt.</p>
<div id="backpack-warning" class="alert alert-warning d-none small p-2 mb-2">
<i class="fas fa-exclamation-triangle me-1"></i> Achtung: Ein Rucksack wurde mehrfach ausgewählt!
</div>
<?php endforeach; ?>
<?php
$user_backpacks_json = [];
$all_assigned_backpack_ids = [];
foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $household_id_for_user);
$current_bp_id = 0;
$user_backpacks_json[$user['id']] = [
'current_id' => $current_bp_id,
'backpacks' => $user_backpacks
];
?>
<div class="mb-3 pb-3 border-bottom">
<div class="form-check mb-2">
<input class="form-check-input participation-check" type="checkbox" name="participate[<?php echo $user['id']; ?>]" value="1" checked id="part_<?php echo $user['id']; ?>">
<label class="form-check-label fw-bold" for="part_<?php echo $user['id']; ?>">
<?php echo htmlspecialchars($user['username']); ?>
</label>
</div>
<div class="ms-4">
<!-- Pass class for JS validation -->
<?php echo render_backpack_card_selector($user, $current_bp_id, $user_backpacks); ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div id="template-notice" class="alert alert-info d-none">
<i class="fas fa-info-circle me-2"></i> Da eine Vorlage gewählt wurde, werden die Träger und Rucksäcke aus der Vorlage übernommen. Du kannst diese später in der Listenansicht bearbeiten.
</div>
</div>
</div>
@@ -194,25 +250,44 @@ require_once 'header.php';
</div>
</div>
<!-- Custom Script for Validation -->
<!-- Custom Script for Validation & Template Logic -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('createListForm');
const submitBtn = document.getElementById('submitBtn');
const warning = document.getElementById('backpack-warning');
const templateSelect = document.getElementById('template_id');
const participantsSection = document.getElementById('participants-section');
const templateNotice = document.getElementById('template-notice');
// Toggle participants section based on template selection
function toggleParticipants() {
if (templateSelect.value !== "0") {
participantsSection.classList.add('d-none');
templateNotice.classList.remove('d-none');
// Disable validation logic effectively by hiding warning
warning.classList.add('d-none');
submitBtn.disabled = false;
} else {
participantsSection.classList.remove('d-none');
templateNotice.classList.add('d-none');
validateBackpacks(); // Re-validate
}
}
templateSelect.addEventListener('change', toggleParticipants);
// Helper function to check duplicates
function validateBackpacks() {
if (templateSelect.value !== "0") return; // Skip if template is selected
const selected = [];
let hasDuplicate = false;
// Find all hidden inputs that store the backpack ID (usually named backpacks[UserID])
// Since render_backpack_card_selector uses a hidden input with name="backpacks[ID]", we select those
// Find all hidden inputs that store the backpack ID
const inputs = document.querySelectorAll('input[name^="backpacks["]');
inputs.forEach(input => {
// Check if user participates
// The input name is backpacks[123], we need to find checkbox participate[123]
const userIdMatch = input.name.match(/backpacks\[(\d+)\]/);
if (userIdMatch) {
const userId = userIdMatch[1];
@@ -244,15 +319,12 @@ document.addEventListener('DOMContentLoaded', function() {
chk.addEventListener('change', validateBackpacks);
});
// Listen for modal close/hide events (bootstrap specific) to re-validate
// Since the backpack selection happens in a modal, validating on modal hide is efficient.
// We use event delegation or attach to body since modals might be dynamically inserted/removed (though here they are static in footer usually)
document.body.addEventListener('hidden.bs.modal', function (event) {
validateBackpacks();
});
// Initial check
validateBackpacks();
toggleParticipants();
});
</script>

View File

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

View File

@@ -6,9 +6,6 @@ function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
$bp = $conn->query("SELECT name FROM backpacks WHERE id = $backpack_id")->fetch_assoc();
// 2. Check/Create Root Item
// Use NULL safe comparison or check for NULL explicitly
// We need to allow for existing items that might be named differently if user renamed them?
// No, we stick to structure.
$root_id = 0;
$stmt = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id = ? AND backpack_id = ?");
$stmt->bind_param("iii", $list_id, $user_id, $backpack_id);
@@ -28,7 +25,8 @@ function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
}
// 3. Sync Compartments
$comps = $conn->query("SELECT id, name FROM backpack_compartments WHERE backpack_id = $backpack_id ORDER BY sort_order ASC");
// FIX: Fetch linked_article_id as well
$comps = $conn->query("SELECT id, name, linked_article_id FROM backpack_compartments WHERE backpack_id = $backpack_id ORDER BY sort_order ASC");
while ($comp = $comps->fetch_assoc()) {
// Check if item exists for this compartment AND this user
$stmt_c = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND backpack_compartment_id = ? AND carrier_user_id = ?");
@@ -36,9 +34,12 @@ function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
$stmt_c->execute();
if ($stmt_c->get_result()->num_rows == 0) {
// Create Compartment Item
$c_name = $comp['name'];
$stmt_ins_c = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, carrier_user_id, name, backpack_compartment_id, parent_packing_list_item_id, quantity, article_id, order_index) VALUES (?, ?, ?, ?, ?, 1, NULL, 0)");
$stmt_ins_c->bind_param("iisii", $list_id, $user_id, $c_name, $comp['id'], $root_id);
$c_name = $comp['name'];
$c_article_id = !empty($comp['linked_article_id']) ? $comp['linked_article_id'] : NULL;
// FIX: Insert linked article_id if present
$stmt_ins_c = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, carrier_user_id, name, backpack_compartment_id, parent_packing_list_item_id, quantity, article_id, order_index) VALUES (?, ?, ?, ?, ?, 1, ?, 0)");
$stmt_ins_c->bind_param("iisiii", $list_id, $user_id, $c_name, $comp['id'], $root_id, $c_article_id);
$stmt_ins_c->execute();
}
}

View File

@@ -32,6 +32,17 @@ 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)
$household_id = null;
$stmt_hh = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
@@ -43,12 +54,12 @@ if ($row = $res_hh->fetch_assoc()) {
}
$backpacks = [];
// AUTO-MIGRATION: Check if product_url column exists (just to be safe for display)
// Ideally handled in edit_backpack but good to be safe.
$check_col = $conn->query("SHOW COLUMNS FROM backpacks LIKE 'product_url'");
$has_product_url = ($check_col && $check_col->num_rows > 0);
$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 = ?";
@@ -85,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">
@@ -109,7 +122,13 @@ 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>

View File

@@ -62,56 +62,10 @@ try {
$new_list_id = $conn->insert_id;
$stmt_new_list->close();
// KORREKTUR: Komplette Überarbeitung der Kopierlogik
// 4. Alle Artikel aus der originalen Liste mit ihrer ID holen
$stmt_items = $conn->prepare("SELECT id, article_id, quantity, parent_packing_list_item_id, carrier_user_id FROM packing_list_items WHERE packing_list_id = ?");
$stmt_items->bind_param("i", $original_list_id);
$stmt_items->execute();
$result_items = $stmt_items->get_result();
$original_items = [];
while ($row = $result_items->fetch_assoc()) {
$original_items[] = $row;
}
$stmt_items->close();
if (!empty($original_items)) {
// 5. Hierarchie-Klonen in zwei Schritten
$old_to_new_id_map = []; // Speichert [alte_item_id] => neue_item_id
// Schritt 5a: Alle Items ohne Parent-Info kopieren und die ID-Zuordnung speichern
$stmt_insert_item = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, article_id, quantity, carrier_user_id) VALUES (?, ?, ?, ?)");
if (!$stmt_insert_item) throw new Exception("DB Prepare Fehler (Item Insert)");
foreach ($original_items as $item) {
$stmt_insert_item->bind_param("iiii", $new_list_id, $item['article_id'], $item['quantity'], $item['carrier_user_id']);
if (!$stmt_insert_item->execute()) throw new Exception("Fehler beim Kopieren von Artikel ID " . $item['article_id']);
$new_item_id = $conn->insert_id;
$old_to_new_id_map[$item['id']] = $new_item_id;
}
$stmt_insert_item->close();
// Schritt 5b: Die Parent-IDs für die neu erstellten Items aktualisieren
$stmt_update_parent = $conn->prepare("UPDATE packing_list_items SET parent_packing_list_item_id = ? WHERE id = ?");
if (!$stmt_update_parent) throw new Exception("DB Prepare Fehler (Parent Update)");
foreach ($original_items as $item) {
// Wenn das Original-Item einen Parent hatte...
if (!empty($item['parent_packing_list_item_id'])) {
$old_parent_id = $item['parent_packing_list_item_id'];
// Stelle sicher, dass der Parent auch in unserer Map existiert
if (isset($old_to_new_id_map[$old_parent_id])) {
$new_parent_id = $old_to_new_id_map[$old_parent_id];
$new_child_id = $old_to_new_id_map[$item['id']];
$stmt_update_parent->bind_param("ii", $new_parent_id, $new_child_id);
if (!$stmt_update_parent->execute()) throw new Exception("Fehler beim Setzen der Hierarchie für Item " . $new_child_id);
}
}
}
$stmt_update_parent->close();
}
// 4. Inhalt kopieren (Items, Träger, Hierarchien)
// Nutzen der zentralen Hilfsfunktion, die auch beim Erstellen aus Templates verwendet wird.
require_once 'packing_list_utils.php';
copyPackingListContent($original_list_id, $new_list_id, $conn);
$conn->commit();
$_SESSION['message'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich zu "'.$copy_name.'" dupliziert!</div>';

View File

@@ -25,11 +25,21 @@ $message = '';
$image_url = '';
$product_url = '';
// AUTO-MIGRATION: Check if product_url column exists, if not add it
// 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;
@@ -98,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']);
@@ -115,7 +154,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if ($_POST['manufacturer_select'] === 'new') {
$new_man = trim($_POST['new_manufacturer_name']);
if (!empty($new_man)) {
// Check if exists first
$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();
@@ -124,7 +162,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if ($res_check->num_rows > 0) {
$manufacturer = $res_check->fetch_assoc()['name'];
} else {
// Save to manufacturers table
$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();
@@ -132,7 +169,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
}
}
} else {
// Look up name from ID
$man_id = intval($_POST['manufacturer_select']);
foreach ($manufacturers as $m) {
if ($m['id'] == $man_id) {
@@ -148,77 +184,49 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$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=?, product_url=? WHERE id=? AND user_id=?");
$stmt->bind_param("sssiisssii", $name, $manufacturer, $model, $weight, $volume, $final_household_id, $image_url_for_db, $product_url_input, $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, product_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("iisssiiss", $user_id, $final_household_id, $name, $manufacturer, $model, $weight, $volume, $image_url_for_db, $product_url_input);
$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 = ?");
@@ -230,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");
@@ -306,13 +325,27 @@ 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>
@@ -346,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>
@@ -414,46 +481,114 @@ document.addEventListener('DOMContentLoaded', function() {
}
if (manSelect) {
// Initial check
toggleManContainer(manSelect.value);
// Vanilla Listener (Backup)
manSelect.addEventListener('change', function() {
toggleManContainer(this.value);
});
manSelect.addEventListener('change', function() { toggleManContainer(this.value); });
new TomSelect(manSelect, { create: false, sortField: { field: "text", direction: "asc" }, onChange: function(value) { toggleManContainer(value); } });
}
// Tom Select Init
if(manSelect) {
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();
@@ -500,4 +635,4 @@ document.addEventListener('DOMContentLoaded', function() {
});
</script>
<?php require_once 'footer.php'; ?>
<?php require_once 'footer.php'; ?>

View File

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

View File

@@ -69,25 +69,32 @@ $stmt_all_articles->execute();
$all_articles_raw = $stmt_all_articles->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_all_articles->close();
$sql_items = "SELECT pli.id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id, pli.backpack_id, pli.backpack_compartment_id, COALESCE(a.name, pli.name) as name, a.weight_grams, a.product_designation, a.consumable, m.name as manufacturer_name
$sql_items = "SELECT pli.id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id, pli.backpack_id, pli.backpack_compartment_id,
COALESCE(
a.name,
bc.name,
CASE WHEN b.name IS NOT NULL THEN CONCAT('Rucksack: ', b.name) ELSE NULL END,
pli.name
) as name,
a.weight_grams, a.product_designation, a.consumable, m.name as manufacturer_name
FROM packing_list_items pli
LEFT JOIN articles a ON pli.article_id = a.id
LEFT JOIN manufacturers m ON a.manufacturer_id = m.id
LEFT JOIN backpacks b ON pli.backpack_id = b.id
LEFT JOIN backpack_compartments bc ON pli.backpack_compartment_id = bc.id
WHERE pli.packing_list_id = ?
AND (pli.carrier_user_id IN ($placeholders) OR pli.carrier_user_id IS NULL)
ORDER BY pli.order_index ASC";
$stmt_items = $conn->prepare($sql_items);
$params_items = array_merge([$packing_list_id], $household_member_ids);
$types_items = 'i' . $types;
$stmt_items->bind_param($types_items, ...$params_items);
$stmt_items->bind_param("i", $packing_list_id);
$stmt_items->execute();
$packed_items_raw = $stmt_items->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_items->close();
$carriers_data = [];
$stmt_carriers = $conn->prepare("SELECT id, username FROM users WHERE id IN ($placeholders)");
$stmt_carriers->bind_param($types, ...$household_member_ids);
// KORREKTUR: Nur Träger anzeigen, die dieser Liste zugewiesen sind
$stmt_carriers = $conn->prepare("SELECT u.id, u.username FROM users u JOIN packing_list_carriers plc ON u.id = plc.user_id WHERE plc.packing_list_id = ? ORDER BY u.username");
$stmt_carriers->bind_param("i", $packing_list_id);
$stmt_carriers->execute();
$carriers_result = $stmt_carriers->get_result();
while ($row = $carriers_result->fetch_assoc()) { $carriers_data[] = $row; }

View File

@@ -60,13 +60,14 @@ $stmt_list_owner->close();
$page_title = "Packliste: " . htmlspecialchars($packing_list['name']);
// FIX: Join Categories also for Backpacks
$sql = "SELECT
pli.id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id,
pli.backpack_id, pli.backpack_compartment_id,
COALESCE(a.name, pli.name, bp.name, bpc.name, 'Unbekanntes Item') AS article_name,
COALESCE(a.weight_grams, bp.weight_grams, 0) as weight_grams,
a.image_url, a.product_designation, a.consumable,
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.id AS carrier_id
@@ -75,6 +76,7 @@ $sql = "SELECT
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 = ?
@@ -182,7 +184,13 @@ function render_item_row($item, $level, $items_by_parent) {
} elseif ($is_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 {
$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 . '">';
@@ -388,19 +396,30 @@ function render_item_row($item, $level, $items_by_parent) {
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"]'));

101
src/packing_list_utils.php Normal file
View File

@@ -0,0 +1,101 @@
<?php
// src/packing_list_utils.php - Hilfsfunktionen für Packlisten
/**
* Kopiert den gesamten Inhalt einer Packliste (Items, Träger, Hierarchie) in eine neue Liste.
*
* @param int $sourceId ID der Quell-Packliste
* @param int $targetId ID der Ziel-Packliste
* @param mysqli $conn Datenbankverbindung
* @throws Exception Bei Datenbankfehlern
*/
function copyPackingListContent($sourceId, $targetId, $conn) {
// 1. Träger kopieren (packing_list_carriers)
$stmt_carriers = $conn->prepare("SELECT user_id, backpack_id FROM packing_list_carriers WHERE packing_list_id = ?");
if (!$stmt_carriers) throw new Exception("Prepare failed for carriers select: " . $conn->error);
$stmt_carriers->bind_param("i", $sourceId);
$stmt_carriers->execute();
$result_carriers = $stmt_carriers->get_result();
$stmt_insert_carrier = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)");
if (!$stmt_insert_carrier) throw new Exception("Prepare failed for carriers insert: " . $conn->error);
while ($row = $result_carriers->fetch_assoc()) {
$stmt_insert_carrier->bind_param("iii", $targetId, $row['user_id'], $row['backpack_id']);
if (!$stmt_insert_carrier->execute()) throw new Exception("Error inserting carrier: " . $stmt_insert_carrier->error);
}
$stmt_carriers->close();
$stmt_insert_carrier->close();
// 2. Items kopieren (packing_list_items)
// Alle Items inklusive Rucksack/Fach-Info holen
$sql_items = "SELECT id, article_id, quantity, parent_packing_list_item_id, carrier_user_id, backpack_id, backpack_compartment_id, name, order_index
FROM packing_list_items WHERE packing_list_id = ?";
$stmt_items = $conn->prepare($sql_items);
if (!$stmt_items) throw new Exception("Prepare failed for items select: " . $conn->error);
$stmt_items->bind_param("i", $sourceId);
$stmt_items->execute();
$result_items = $stmt_items->get_result();
$original_items = [];
while ($row = $result_items->fetch_assoc()) {
$original_items[] = $row;
}
$stmt_items->close();
if (!empty($original_items)) {
$old_to_new_id_map = []; // Mapping: alte_ID => neue_ID
// 2a. Items einfügen (zunächst ohne Parent-Referenz)
$sql_insert = "INSERT INTO packing_list_items
(packing_list_id, article_id, quantity, carrier_user_id, backpack_id, backpack_compartment_id, name, order_index)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
$stmt_insert_item = $conn->prepare($sql_insert);
if (!$stmt_insert_item) throw new Exception("Prepare failed for item insert: " . $conn->error);
foreach ($original_items as $item) {
$stmt_insert_item->bind_param(
"iiiisiis",
$targetId,
$item['article_id'],
$item['quantity'],
$item['carrier_user_id'],
$item['backpack_id'],
$item['backpack_compartment_id'],
$item['name'],
$item['order_index']
);
if (!$stmt_insert_item->execute()) {
throw new Exception("Error copying item ID " . $item['article_id'] . ": " . $stmt_insert_item->error);
}
$old_to_new_id_map[$item['id']] = $conn->insert_id;
}
$stmt_insert_item->close();
// 2b. Parent-Referenzen aktualisieren (Hierarchie wiederherstellen)
$stmt_update_parent = $conn->prepare("UPDATE packing_list_items SET parent_packing_list_item_id = ? WHERE id = ?");
if (!$stmt_update_parent) throw new Exception("Prepare failed for parent update: " . $conn->error);
foreach ($original_items as $item) {
if (!empty($item['parent_packing_list_item_id'])) {
$old_parent_id = $item['parent_packing_list_item_id'];
// Prüfen, ob der Parent auch kopiert wurde (sollte immer so sein)
if (isset($old_to_new_id_map[$old_parent_id])) {
$new_parent_id = $old_to_new_id_map[$old_parent_id];
$new_child_id = $old_to_new_id_map[$item['id']];
$stmt_update_parent->bind_param("ii", $new_parent_id, $new_child_id);
if (!$stmt_update_parent->execute()) {
throw new Exception("Error updating parent hierarchy: " . $stmt_update_parent->error);
}
}
}
}
$stmt_update_parent->close();
}
}
?>

View File

@@ -1,6 +1,5 @@
<?php
// packing_lists.php - Übersicht über alle Haushalts-Packlisten
// KORREKTUR: Bearbeiten-Buttons werden nun auch für Haushaltsmitglieder bei geteilten Listen angezeigt.
// packing_lists.php - Übersicht über alle Haushalts-Packlisten und Vorlagen
$page_title = "Packlisten des Haushalts";
@@ -16,8 +15,18 @@ require_once 'db_connect.php';
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
// AUTO-MIGRATION: Check columns to prevent SQL errors
$check_col = $conn->query("SHOW COLUMNS FROM packing_lists LIKE 'is_template'");
if ($check_col && $check_col->num_rows == 0) {
// Add column if missing
$conn->query("ALTER TABLE packing_lists ADD COLUMN is_template TINYINT(1) NOT NULL DEFAULT 0 AFTER share_token");
}
$view = isset($_GET['view']) && $_GET['view'] === 'templates' ? 'templates' : 'lists';
$is_template_view = ($view === 'templates');
$message = '';
if (isset($_SESSION['message'])) {
$message = $_SESSION['message'];
unset($_SESSION['message']);
@@ -30,25 +39,11 @@ $stmt_household->execute();
$current_user_household_id = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
$household_member_ids = [$current_user_id];
if ($current_user_household_id) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $current_user_household_id);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) {
if (!in_array($row['id'], $household_member_ids)) {
$household_member_ids[] = $row['id'];
}
}
$stmt_members->close();
}
$packing_lists = [];
// KORREKTUR: Die SQL-Abfrage wurde vereinfacht und korrigiert, um alle relevanten Listen anzuzeigen.
// Es werden alle Listen angezeigt, die entweder vom Nutzer erstellt wurden ODER mit seinem Haushalt geteilt sind.
$is_template_val = $is_template_view ? 1 : 0;
$sql = "SELECT
pl.id, pl.name, pl.description, pl.user_id, pl.household_id,
pl.id, pl.name, pl.description, pl.user_id, pl.household_id, pl.is_template,
u.username as creator_name,
COUNT(DISTINCT COALESCE(pli.carrier_user_id, 'sonstiges')) AS carrier_count,
SUM(pli.quantity * a.weight_grams) AS total_weight
@@ -56,15 +51,17 @@ $sql = "SELECT
JOIN users u ON pl.user_id = u.id
LEFT JOIN packing_list_items pli ON pl.id = pli.packing_list_id
LEFT JOIN articles a ON pli.article_id = a.id
WHERE pl.user_id = ? OR pl.household_id = ?
GROUP BY pl.id, pl.name, pl.description, pl.user_id, u.username, pl.household_id
WHERE (pl.user_id = ? OR pl.household_id = ?) AND pl.is_template = ?
GROUP BY pl.id, pl.name, pl.description, pl.user_id, u.username, pl.household_id, pl.is_template
ORDER BY pl.name ASC";
$stmt = $conn->prepare($sql);
if ($stmt === false) {
$message .= '<div class="alert alert-danger" role="alert">SQL Prepare-Fehler: ' . $conn->error . '</div>';
} else {
$stmt->bind_param("ii", $current_user_id, $current_user_household_id);
// Falls household_id NULL ist, nutzen wir 0 (da ID immer > 0)
$h_id = $current_user_household_id ?: 0;
$stmt->bind_param("iii", $current_user_id, $h_id, $is_template_val);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
@@ -76,28 +73,53 @@ $conn->close();
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Packlisten im Haushalt</h2>
<a href="add_packing_list.php" class="btn btn-sm btn-outline-light"><i class="fas fa-plus-circle me-2"></i>Neue Packliste erstellen</a>
<div class="card-header">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h4 mb-0"><?php echo $is_template_view ? 'Meine Vorlagen' : 'Packlisten im Haushalt'; ?></h2>
<a href="add_packing_list.php" class="btn btn-sm btn-outline-light"><i class="fas fa-plus-circle me-2"></i>Neue Packliste erstellen</a>
</div>
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link <?php echo !$is_template_view ? 'active' : ''; ?>" href="packing_lists.php?view=lists">
<i class="fas fa-list me-2"></i>Packlisten
</a>
</li>
<li class="nav-item">
<a class="nav-link <?php echo $is_template_view ? 'active' : ''; ?>" href="packing_lists.php?view=templates">
<i class="fas fa-copy me-2"></i>Vorlagen
</a>
</li>
</ul>
</div>
<div class="card-body">
<?php if(!empty($message)) echo $message; ?>
<?php if (empty($packing_lists)): ?>
<div class="alert alert-info text-center" role="alert">
Du hast noch keine Packlisten angelegt. <a href="add_packing_list.php" class="alert-link">Erstelle jetzt deine erste!</a>
<?php if ($is_template_view): ?>
Du hast noch keine Vorlagen. Erstelle eine Packliste und speichere sie als Vorlage.
<?php else: ?>
Du hast noch keine Packlisten angelegt. <a href="add_packing_list.php" class="alert-link">Erstelle jetzt deine erste!</a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="row g-4">
<?php foreach ($packing_lists as $list): ?>
<div class="col-md-6 col-lg-4">
<div class="card h-100 shadow-sm packing-list-card">
<div class="card h-100 shadow-sm packing-list-card <?php echo $is_template_view ? 'border-info' : ''; ?>">
<div class="card-body d-flex flex-column">
<img src="assets/images/rucksack_icon.png" alt="Packliste" class="card-icon">
<div class="d-flex justify-content-between align-items-start mb-2">
<img src="assets/images/rucksack_icon.png" alt="Packliste" class="card-icon" style="opacity: <?php echo $is_template_view ? '0.7' : '1'; ?>;">
<?php if ($is_template_view): ?>
<span class="badge bg-info text-dark">VORLAGE</span>
<?php endif; ?>
</div>
<h5 class="card-title"><?php echo htmlspecialchars($list['name']); ?></h5>
<p class="card-text text-muted flex-grow-1 small"><?php echo htmlspecialchars($list['description'] ?: 'Keine Beschreibung vorhanden.'); ?></p>
<div class="mt-auto d-flex justify-content-between">
<span class="badge bg-secondary creator-badge"><i class="fas fa-user-edit me-1"></i> Erstellt von <?php echo htmlspecialchars($list['creator_name']); ?></span>
<span class="badge bg-secondary creator-badge"><i class="fas fa-user-edit me-1"></i> <?php echo htmlspecialchars($list['creator_name']); ?></span>
<?php if (!empty($list['household_id'])): ?>
<span class="badge bg-success" data-bs-toggle="tooltip" title="Für den Haushalt freigegeben"><i class="fas fa-users"></i></span>
<?php else: ?>
@@ -111,19 +133,46 @@ $conn->close();
<span data-bs-toggle="tooltip" title="Gesamtgewicht"><i class="fas fa-weight-hanging text-muted"></i> <span class="badge bg-success rounded-pill ms-1"><?php echo number_format(($list['total_weight'] ?? 0) / 1000, 2, ',', '.'); ?> kg</span></span>
</div>
<div>
<a href="packing_list_detail.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-success" title="Details ansehen"><i class="fas fa-eye"></i></a>
<?php
$is_owner = ($list['user_id'] == $current_user_id);
$can_edit_household_list = (!empty($list['household_id']) && $list['household_id'] == $current_user_household_id);
// Actions für Vorlagen und Listen unterscheiden
if ($is_template_view) {
// VORLAGEN ACTIONS
?>
<form action="add_packing_list.php" method="POST" class="d-inline">
</form>
<?php if ($is_owner || $can_edit_household_list): ?>
<a href="edit_packing_list_details.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-primary" title="Details bearbeiten"><i class="fas fa-pencil-alt"></i></a>
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Inhalt bearbeiten"><i class="fas fa-boxes"></i></a>
<?php endif; ?>
<?php
} else {
// LIST ACTIONS (Standard)
?>
<a href="packing_list_detail.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-success" title="Details ansehen"><i class="fas fa-eye"></i></a>
<?php if ($is_owner || $can_edit_household_list): ?>
<a href="edit_packing_list_details.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-primary" title="Details bearbeiten"><i class="fas fa-pencil-alt"></i></a>
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Artikel verwalten"><i class="fas fa-boxes"></i></a>
<?php endif; ?>
<?php if ($is_owner): ?>
<!-- Save As Template -->
<a href="save_as_template.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-warning" title="Als Vorlage speichern"><i class="fas fa-save"></i></a>
<?php endif; ?>
<?php
}
?>
<?php if ($is_owner || $can_edit_household_list): ?>
<a href="edit_packing_list_details.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-primary" title="Details bearbeiten"><i class="fas fa-pencil-alt"></i></a>
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Artikel verwalten"><i class="fas fa-boxes"></i></a>
<?php endif; ?>
<?php if ($is_owner): ?>
<a href="share_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-info" title="Teilen"><i class="fas fa-share-alt"></i></a>
<a href="duplicate_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-dark" title="Duplizieren" onclick="return confirm('Möchten Sie diese Packliste wirklich duplizieren?');"><i class="fas fa-copy"></i></a>
<a href="delete_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-danger ms-2" title="Löschen" onclick="return confirm('Sind Sie sicher, dass Sie diese Packliste löschen möchten?');"><i class="fas fa-trash"></i></a>
<?php if (!$is_template_view): ?>
<a href="duplicate_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-dark" title="Duplizieren" onclick="return confirm('Möchten Sie diese Packliste wirklich duplizieren?');"><i class="fas fa-copy"></i></a>
<?php endif; ?>
<a href="delete_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-danger ms-2" title="Löschen" onclick="return confirm('Sind Sie sicher, dass Sie diese <?php echo $is_template_view ? 'Vorlage' : 'Packliste'; ?> löschen möchten?');"><i class="fas fa-trash"></i></a>
<?php endif; ?>
</div>
</div>
@@ -135,4 +184,4 @@ $conn->close();
</div>
</div>
<?php require_once 'footer.php'; ?>
<?php require_once 'footer.php'; ?>

58
src/save_as_template.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
// src/save_as_template.php - Speichert eine Packliste als Vorlage (Kopie)
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once 'db_connect.php';
require_once 'packing_list_utils.php';
$current_user_id = $_SESSION['user_id'];
$list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if ($list_id <= 0) {
header("Location: packing_lists.php");
exit;
}
// Prüfen, ob Liste existiert und User Zugriff hat
$stmt = $conn->prepare("SELECT name, description, household_id FROM packing_lists WHERE id = ? AND (user_id = ? OR household_id = (SELECT household_id FROM users WHERE id = ?))");
// Eigentlich darf nur Owner oder Household-Admin kopieren? Lassen wir es für alle mit Lesezugriff zu.
$stmt->bind_param("iii", $list_id, $current_user_id, $current_user_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
$_SESSION['message'] = '<div class="alert alert-danger">Packliste nicht gefunden oder keine Berechtigung.</div>';
header("Location: packing_lists.php");
exit;
}
$list = $result->fetch_assoc();
$stmt->close();
$new_name = "Vorlage: " . $list['name'];
// Neue Liste (Template) anlegen
$stmt_insert = $conn->prepare("INSERT INTO packing_lists (user_id, household_id, name, description, is_template) VALUES (?, ?, ?, ?, 1)");
$stmt_insert->bind_param("isss", $current_user_id, $list['household_id'], $new_name, $list['description']);
if ($stmt_insert->execute()) {
$new_template_id = $conn->insert_id;
try {
copyPackingListContent($list_id, $new_template_id, $conn);
$_SESSION['message'] = '<div class="alert alert-success">Packliste erfolgreich als Vorlage gespeichert! Du findest sie im Tab "Vorlagen".</div>';
} catch (Exception $e) {
// Aufräumen bei Fehler wäre gut, aber hier einfach Fehler melden
$_SESSION['message'] = '<div class="alert alert-warning">Vorlage erstellt, aber Inhalt konnte nicht vollständig kopiert werden: ' . $e->getMessage() . '</div>';
}
} else {
$_SESSION['message'] = '<div class="alert alert-danger">Fehler beim Erstellen der Vorlage: ' . $conn->error . '</div>';
}
header("Location: packing_lists.php?view=templates");
exit;
?>