Compare commits
16 Commits
0c4cc705a6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7744168d0d | ||
|
|
7bdc421d90 | ||
|
|
0753e2c4b9 | ||
|
|
a19cfa98f0 | ||
| 8c50910363 | |||
|
|
59104fb3bd | ||
|
|
b9446b01a3 | ||
|
|
a4b74ab480 | ||
|
|
991f264e29 | ||
|
|
80ede8e3a6 | ||
|
|
70eef4cd82 | ||
|
|
967bae965a | ||
|
|
f9df3087a7 | ||
|
|
dd1cbcb2a6 | ||
|
|
f1d9634dba | ||
|
|
36660fdd51 |
@@ -6,7 +6,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
28
README.md
28
README.md
@@ -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.
|
||||
|
||||
@@ -75,9 +75,12 @@ CREATE TABLE `backpack_compartments` (
|
||||
`backpack_id` int(11) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`sort_order` int(11) DEFAULT 0,
|
||||
`linked_article_id` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `backpack_id` (`backpack_id`),
|
||||
CONSTRAINT `backpack_compartments_ibfk_1` FOREIGN KEY (`backpack_id`) REFERENCES `backpacks` (`id`) ON DELETE CASCADE
|
||||
KEY `fk_bc_article` (`linked_article_id`),
|
||||
CONSTRAINT `backpack_compartments_ibfk_1` FOREIGN KEY (`backpack_id`) REFERENCES `backpacks` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_bc_article` FOREIGN KEY (`linked_article_id`) REFERENCES `articles` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
@@ -108,10 +111,14 @@ CREATE TABLE `backpacks` (
|
||||
`weight_grams` int(11) DEFAULT 0,
|
||||
`volume_liters` int(11) DEFAULT 0,
|
||||
`image_url` varchar(255) DEFAULT NULL,
|
||||
`product_url` varchar(255) DEFAULT NULL,
|
||||
`category_id` int(11) DEFAULT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
CONSTRAINT `backpacks_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
KEY `fk_bp_category` (`category_id`),
|
||||
CONSTRAINT `backpacks_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_bp_category` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
@@ -395,6 +402,7 @@ CREATE TABLE `packing_lists` (
|
||||
`name` varchar(255) NOT NULL,
|
||||
`description` text DEFAULT NULL,
|
||||
`share_token` varchar(255) DEFAULT NULL,
|
||||
`is_template` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `share_token` (`share_token`),
|
||||
|
||||
@@ -1,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" }
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -224,7 +224,33 @@ function user_can_edit_list($conn, $packing_list_id, $user_id) {
|
||||
}
|
||||
|
||||
function get_all_items($conn, $packing_list_id) {
|
||||
$stmt = $conn->prepare("SELECT pli.id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id, pli.backpack_id, pli.backpack_compartment_id, COALESCE(a.name, pli.name) as name, a.weight_grams, a.product_designation, a.consumable, m.name as manufacturer_name FROM packing_list_items pli LEFT JOIN articles a ON pli.article_id = a.id LEFT JOIN manufacturers m ON a.manufacturer_id = m.id WHERE pli.packing_list_id = ? ORDER BY pli.order_index ASC, pli.id ASC");
|
||||
$sql = "SELECT
|
||||
pli.id,
|
||||
pli.article_id,
|
||||
pli.quantity,
|
||||
pli.parent_packing_list_item_id,
|
||||
pli.carrier_user_id,
|
||||
pli.backpack_id,
|
||||
pli.backpack_compartment_id,
|
||||
COALESCE(
|
||||
a.name,
|
||||
bc.name,
|
||||
CASE WHEN b.name IS NOT NULL THEN CONCAT('Rucksack: ', b.name) ELSE NULL END,
|
||||
pli.name
|
||||
) as name,
|
||||
a.weight_grams,
|
||||
a.product_designation,
|
||||
a.consumable,
|
||||
m.name as manufacturer_name
|
||||
FROM packing_list_items pli
|
||||
LEFT JOIN articles a ON pli.article_id = a.id
|
||||
LEFT JOIN manufacturers m ON a.manufacturer_id = m.id
|
||||
LEFT JOIN backpacks b ON pli.backpack_id = b.id
|
||||
LEFT JOIN backpack_compartments bc ON pli.backpack_compartment_id = bc.id
|
||||
WHERE pli.packing_list_id = ?
|
||||
ORDER BY pli.order_index ASC, pli.id ASC";
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param("i", $packing_list_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
|
||||
@@ -6,9 +6,6 @@ function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
|
||||
$bp = $conn->query("SELECT name FROM backpacks WHERE id = $backpack_id")->fetch_assoc();
|
||||
|
||||
// 2. Check/Create Root Item
|
||||
// Use NULL safe comparison or check for NULL explicitly
|
||||
// We need to allow for existing items that might be named differently if user renamed them?
|
||||
// No, we stick to structure.
|
||||
$root_id = 0;
|
||||
$stmt = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id = ? AND backpack_id = ?");
|
||||
$stmt->bind_param("iii", $list_id, $user_id, $backpack_id);
|
||||
@@ -28,7 +25,8 @@ function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
|
||||
}
|
||||
|
||||
// 3. Sync Compartments
|
||||
$comps = $conn->query("SELECT id, name FROM backpack_compartments WHERE backpack_id = $backpack_id ORDER BY sort_order ASC");
|
||||
// FIX: Fetch linked_article_id as well
|
||||
$comps = $conn->query("SELECT id, name, linked_article_id FROM backpack_compartments WHERE backpack_id = $backpack_id ORDER BY sort_order ASC");
|
||||
while ($comp = $comps->fetch_assoc()) {
|
||||
// Check if item exists for this compartment AND this user
|
||||
$stmt_c = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND backpack_compartment_id = ? AND carrier_user_id = ?");
|
||||
@@ -36,9 +34,12 @@ function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
|
||||
$stmt_c->execute();
|
||||
if ($stmt_c->get_result()->num_rows == 0) {
|
||||
// Create Compartment Item
|
||||
$c_name = $comp['name'];
|
||||
$stmt_ins_c = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, carrier_user_id, name, backpack_compartment_id, parent_packing_list_item_id, quantity, article_id, order_index) VALUES (?, ?, ?, ?, ?, 1, NULL, 0)");
|
||||
$stmt_ins_c->bind_param("iisii", $list_id, $user_id, $c_name, $comp['id'], $root_id);
|
||||
$c_name = $comp['name'];
|
||||
$c_article_id = !empty($comp['linked_article_id']) ? $comp['linked_article_id'] : NULL;
|
||||
|
||||
// FIX: Insert linked article_id if present
|
||||
$stmt_ins_c = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, carrier_user_id, name, backpack_compartment_id, parent_packing_list_item_id, quantity, article_id, order_index) VALUES (?, ?, ?, ?, ?, 1, ?, 0)");
|
||||
$stmt_ins_c->bind_param("iisiii", $list_id, $user_id, $c_name, $comp['id'], $root_id, $c_article_id);
|
||||
$stmt_ins_c->execute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>';
|
||||
|
||||
@@ -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'; ?>
|
||||
@@ -12,7 +12,7 @@ if (!isset($_SESSION['user_id'])) {
|
||||
|
||||
require_once 'db_connect.php';
|
||||
require_once 'household_actions.php';
|
||||
require_once 'backpack_utils.php'; // Fix: Include utils
|
||||
require_once 'backpack_utils.php';
|
||||
require_once 'header.php';
|
||||
|
||||
$current_user_id = $_SESSION['user_id'];
|
||||
@@ -29,7 +29,7 @@ if ($packing_list_id > 0) {
|
||||
$current_user_household_id = $stmt_household_check->get_result()->fetch_assoc()['household_id'];
|
||||
$stmt_household_check->close();
|
||||
|
||||
$stmt_list_check = $conn->prepare("SELECT id, name, description, user_id, household_id FROM packing_lists WHERE id = ?");
|
||||
$stmt_list_check = $conn->prepare("SELECT id, name, description, user_id, household_id, is_template FROM packing_lists WHERE id = ?");
|
||||
$stmt_list_check->bind_param("i", $packing_list_id);
|
||||
$stmt_list_check->execute();
|
||||
$result = $stmt_list_check->get_result();
|
||||
@@ -85,61 +85,73 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
|
||||
// Update Basic Details
|
||||
$name = trim($_POST['name']);
|
||||
$description = trim($_POST['description']);
|
||||
$stmt_update = $conn->prepare("UPDATE packing_lists SET name = ?, description = ? WHERE id = ?");
|
||||
$stmt_update->bind_param("ssi", $name, $description, $packing_list_id);
|
||||
|
||||
// Household sharing logic
|
||||
$new_household_id = NULL;
|
||||
if (isset($_POST['is_household_list']) && $_POST['is_household_list'] == '1' && $current_user_household_id) {
|
||||
$new_household_id = $current_user_household_id;
|
||||
}
|
||||
|
||||
$stmt_update = $conn->prepare("UPDATE packing_lists SET name = ?, description = ?, household_id = ? WHERE id = ?");
|
||||
$stmt_update->bind_param("ssii", $name, $description, $new_household_id, $packing_list_id);
|
||||
$stmt_update->execute();
|
||||
$packing_list['name'] = $name;
|
||||
$packing_list['description'] = $description;
|
||||
$packing_list['household_id'] = $new_household_id;
|
||||
|
||||
// Handle Backpack Assignments
|
||||
if (isset($_POST['backpacks'])) {
|
||||
foreach ($_POST['backpacks'] as $uid => $bid) {
|
||||
$uid = intval($uid);
|
||||
$bid = intval($bid);
|
||||
// Handle Participation & Backpacks
|
||||
// Get all potential users to check if they were unchecked
|
||||
$participate_map = $_POST['participate'] ?? []; // Array of UserID => "1" if checked
|
||||
|
||||
foreach ($available_users as $user) {
|
||||
$uid = $user['id'];
|
||||
$is_checked = isset($participate_map[$uid]);
|
||||
$was_participating = array_key_exists($uid, $current_assignments);
|
||||
|
||||
if ($is_checked) {
|
||||
// User participates -> Update/Insert Backpack
|
||||
$bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0;
|
||||
$bid = ($bid > 0) ? $bid : NULL;
|
||||
|
||||
// Update Carrier Table
|
||||
// Check if exists
|
||||
$old_backpack_id = 0;
|
||||
$stmt_chk = $conn->prepare("SELECT id, backpack_id FROM packing_list_carriers WHERE packing_list_id = ? AND user_id = ?");
|
||||
$stmt_chk->bind_param("ii", $packing_list_id, $uid);
|
||||
$stmt_chk->execute();
|
||||
$res_chk = $stmt_chk->get_result();
|
||||
if ($row_chk = $res_chk->fetch_assoc()) {
|
||||
$old_backpack_id = $row_chk['backpack_id'];
|
||||
$stmt_up = $conn->prepare("UPDATE packing_list_carriers SET backpack_id = ? WHERE packing_list_id = ? AND user_id = ?");
|
||||
$stmt_up->bind_param("iii", $bid, $packing_list_id, $uid);
|
||||
$stmt_up->execute();
|
||||
|
||||
if ($was_participating) {
|
||||
$old_bid = $current_assignments[$uid];
|
||||
// Update if changed
|
||||
if ($old_bid != $bid) {
|
||||
$stmt_up = $conn->prepare("UPDATE packing_list_carriers SET backpack_id = ? WHERE packing_list_id = ? AND user_id = ?");
|
||||
$stmt_up->bind_param("iii", $bid, $packing_list_id, $uid);
|
||||
$stmt_up->execute();
|
||||
|
||||
// Cleanup Old Containers
|
||||
cleanup_old_backpack_containers($conn, $packing_list_id, $uid);
|
||||
|
||||
// Sync New
|
||||
if ($bid) {
|
||||
sync_backpack_items($conn, $packing_list_id, $uid, $bid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// New Participant
|
||||
$stmt_in = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)");
|
||||
$stmt_in->bind_param("iii", $packing_list_id, $uid, $bid);
|
||||
$stmt_in->execute();
|
||||
}
|
||||
|
||||
// CLEANUP LOGIC: If backpack changed or removed
|
||||
if ($old_backpack_id != $bid) {
|
||||
// 1. Unparent all items that are inside the old containers (so they don't get deleted)
|
||||
// Find all container items for this user
|
||||
$stmt_find_containers = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id = ? AND (backpack_id IS NOT NULL OR backpack_compartment_id IS NOT NULL)");
|
||||
$stmt_find_containers->bind_param("ii", $packing_list_id, $uid);
|
||||
$stmt_find_containers->execute();
|
||||
$res_cont = $stmt_find_containers->get_result();
|
||||
$container_ids = [];
|
||||
while ($r = $res_cont->fetch_assoc()) $container_ids[] = $r['id'];
|
||||
|
||||
if (!empty($container_ids)) {
|
||||
$ids_str = implode(',', $container_ids);
|
||||
// Set parent to NULL for children of these containers
|
||||
$conn->query("UPDATE packing_list_items SET parent_packing_list_item_id = NULL WHERE packing_list_id = $packing_list_id AND parent_packing_list_item_id IN ($ids_str)");
|
||||
|
||||
// 2. Delete the containers
|
||||
$conn->query("DELETE FROM packing_list_items WHERE id IN ($ids_str)");
|
||||
if ($bid) {
|
||||
sync_backpack_items($conn, $packing_list_id, $uid, $bid);
|
||||
}
|
||||
}
|
||||
|
||||
// SYNC LOGIC (Only if new backpack assigned)
|
||||
if ($bid && $old_backpack_id != $bid) {
|
||||
sync_backpack_items($conn, $packing_list_id, $uid, $bid);
|
||||
} else {
|
||||
// User NOT checked -> Remove if existed
|
||||
if ($was_participating) {
|
||||
$conn->query("DELETE FROM packing_list_carriers WHERE packing_list_id = $packing_list_id AND user_id = $uid");
|
||||
// Cleanup Items: Delete all items carried by this user?
|
||||
// Or move to unassigned? Let's move to unassigned (NULL) to be safe against data loss.
|
||||
// But Containers (Backpacks) should be deleted.
|
||||
|
||||
// 1. Delete Containers
|
||||
cleanup_old_backpack_containers($conn, $packing_list_id, $uid);
|
||||
|
||||
// 2. Move remaining items (loose items) to unassigned?
|
||||
$conn->query("UPDATE packing_list_items SET carrier_user_id = NULL WHERE packing_list_id = $packing_list_id AND carrier_user_id = $uid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,18 +166,41 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup_old_backpack_containers($conn, $list_id, $user_id) {
|
||||
// Find all container items for this user (backpacks or compartments)
|
||||
$stmt = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id = ? AND (backpack_id IS NOT NULL OR backpack_compartment_id IS NOT NULL)");
|
||||
$stmt->bind_param("ii", $list_id, $user_id);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
$container_ids = [];
|
||||
while ($r = $res->fetch_assoc()) $container_ids[] = $r['id'];
|
||||
|
||||
if (!empty($container_ids)) {
|
||||
$ids_str = implode(',', $container_ids);
|
||||
// Unparent children so they don't get deleted (or keep them and they get deleted? No, save content)
|
||||
// Set parent to NULL for children of these containers
|
||||
$conn->query("UPDATE packing_list_items SET parent_packing_list_item_id = NULL WHERE packing_list_id = $list_id AND parent_packing_list_item_id IN ($ids_str)");
|
||||
|
||||
// Delete containers
|
||||
$conn->query("DELETE FROM packing_list_items WHERE id IN ($ids_str)");
|
||||
}
|
||||
}
|
||||
|
||||
$back_link = 'packing_lists.php' . (!empty($packing_list['is_template']) ? '?view=templates' : '');
|
||||
$page_headline = !empty($packing_list['is_template']) ? 'Vorlage bearbeiten' : 'Details bearbeiten';
|
||||
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="h4 mb-0">Details: <?php echo htmlspecialchars($packing_list['name'] ?? ''); ?></h2>
|
||||
<a href="packing_lists.php" class="btn btn-sm btn-outline-light"><i class="fas fa-arrow-left me-2"></i>Zurück</a>
|
||||
<h2 class="h4 mb-0"><?php echo $page_headline; ?>: <?php echo htmlspecialchars($packing_list['name'] ?? ''); ?></h2>
|
||||
<a href="<?php echo $back_link; ?>" class="btn btn-sm btn-outline-light"><i class="fas fa-arrow-left me-2"></i>Zurück</a>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<?php echo $message; ?>
|
||||
|
||||
<?php if ($packing_list): ?>
|
||||
<form method="post">
|
||||
<form method="post" id="editListForm">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h5 class="mb-3">Basisdaten</h5>
|
||||
@@ -177,38 +212,51 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
|
||||
<label class="form-label">Beschreibung</label>
|
||||
<textarea class="form-control" name="description" rows="3"><?php echo htmlspecialchars($packing_list['description'] ?: ''); ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php if ($current_user_household_id): ?>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="is_household_list" name="is_household_list" value="1" <?php echo !empty($packing_list['household_id']) ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="is_household_list">Für den gesamten Haushalt freigeben</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h5 class="mb-3">Rucksack-Zuweisung</h5>
|
||||
<h5 class="mb-3">Teilnehmer & Rucksäcke <?php if(!empty($packing_list['is_template'])): ?><small class="text-muted">(Standard)</small><?php endif; ?></h5>
|
||||
<div class="card bg-light border-0">
|
||||
<div class="card-body">
|
||||
<p class="small text-muted">Wähle hier, wer welchen Rucksack trägt. Bereits vergebene Rucksäcke werden ausgeblendet.</p>
|
||||
<p class="small text-muted">Wähle hier, wer mitkommt und welchen Rucksack er trägt.</p>
|
||||
|
||||
<div id="backpack-warning" class="alert alert-warning d-none small p-2 mb-2">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i> Ein Rucksack wurde mehrfach ausgewählt!
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Calculate used backpacks
|
||||
$all_assigned_backpack_ids = array_values($current_assignments);
|
||||
|
||||
// Prepare data for JS (still needed for the render helper)
|
||||
$user_backpacks_json = [];
|
||||
|
||||
foreach ($available_users as $user):
|
||||
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $packing_list['household_id']);
|
||||
$is_participating = array_key_exists($user['id'], $current_assignments);
|
||||
$my_current_bp_id = $current_assignments[$user['id']] ?? 0;
|
||||
|
||||
// Store for JS
|
||||
$user_backpacks_json[$user['id']] = [
|
||||
'current_id' => $my_current_bp_id,
|
||||
'backpacks' => $user_backpacks
|
||||
];
|
||||
|
||||
// Use helper to render the widget
|
||||
echo '<div class="mb-4 border-bottom pb-3">';
|
||||
echo '<label class="form-label fw-bold mb-2">' . htmlspecialchars($user['username']) . '</label>';
|
||||
echo render_backpack_card_selector($user, $my_current_bp_id, $user_backpacks);
|
||||
echo '</div>';
|
||||
endforeach;
|
||||
?>
|
||||
<div class="mb-4 border-bottom pb-3">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input participation-check" type="checkbox" name="participate[<?php echo $user['id']; ?>]" value="1" <?php echo $is_participating ? 'checked' : ''; ?> id="part_<?php echo $user['id']; ?>">
|
||||
<label class="form-check-label fw-bold" for="part_<?php echo $user['id']; ?>">
|
||||
<?php echo htmlspecialchars($user['username']); ?>
|
||||
</label>
|
||||
</div>
|
||||
<div class="ms-4 backpack-selector-wrapper" style="<?php echo $is_participating ? '' : 'opacity: 0.5; pointer-events: none;'; ?>">
|
||||
<?php echo render_backpack_card_selector($user, $my_current_bp_id, $user_backpacks); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,7 +266,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="manage_packing_list_items.php?id=<?php echo $packing_list_id; ?>" class="btn btn-info text-white"><i class="fas fa-boxes me-2"></i>Inhalt bearbeiten</a>
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-save me-2"></i>Speichern & Synchronisieren</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn"><i class="fas fa-save me-2"></i>Speichern & Synchronisieren</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
@@ -228,4 +276,63 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
|
||||
<!-- Render Modal JS -->
|
||||
<?php echo render_backpack_modal_script($user_backpacks_json, $all_assigned_backpack_ids); ?>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const warning = document.getElementById('backpack-warning');
|
||||
|
||||
// Toggle Opacity logic
|
||||
document.querySelectorAll('.participation-check').forEach(chk => {
|
||||
chk.addEventListener('change', function() {
|
||||
const wrapper = this.closest('.mb-4').querySelector('.backpack-selector-wrapper');
|
||||
if (this.checked) {
|
||||
wrapper.style.opacity = '1';
|
||||
wrapper.style.pointerEvents = 'auto';
|
||||
} else {
|
||||
wrapper.style.opacity = '0.5';
|
||||
wrapper.style.pointerEvents = 'none';
|
||||
}
|
||||
validateBackpacks();
|
||||
});
|
||||
});
|
||||
|
||||
// Validation Logic (Copied from add_packing_list.php)
|
||||
function validateBackpacks() {
|
||||
const selected = [];
|
||||
let hasDuplicate = false;
|
||||
|
||||
const inputs = document.querySelectorAll('input[name^="backpacks["]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
const userIdMatch = input.name.match(/backpacks\[(\d+)\]/);
|
||||
if (userIdMatch) {
|
||||
const userId = userIdMatch[1];
|
||||
const partCheck = document.getElementById('part_' + userId);
|
||||
|
||||
if (partCheck && partCheck.checked) {
|
||||
const val = parseInt(input.value);
|
||||
if (val > 0) {
|
||||
if (selected.includes(val)) {
|
||||
hasDuplicate = true;
|
||||
}
|
||||
selected.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasDuplicate) {
|
||||
warning.classList.remove('d-none');
|
||||
submitBtn.disabled = true;
|
||||
} else {
|
||||
warning.classList.add('d-none');
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener('hidden.bs.modal', validateBackpacks);
|
||||
validateBackpacks();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once 'footer.php'; ?>
|
||||
@@ -69,25 +69,32 @@ $stmt_all_articles->execute();
|
||||
$all_articles_raw = $stmt_all_articles->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
$stmt_all_articles->close();
|
||||
|
||||
$sql_items = "SELECT pli.id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id, pli.backpack_id, pli.backpack_compartment_id, COALESCE(a.name, pli.name) as name, a.weight_grams, a.product_designation, a.consumable, m.name as manufacturer_name
|
||||
$sql_items = "SELECT pli.id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id, pli.backpack_id, pli.backpack_compartment_id,
|
||||
COALESCE(
|
||||
a.name,
|
||||
bc.name,
|
||||
CASE WHEN b.name IS NOT NULL THEN CONCAT('Rucksack: ', b.name) ELSE NULL END,
|
||||
pli.name
|
||||
) as name,
|
||||
a.weight_grams, a.product_designation, a.consumable, m.name as manufacturer_name
|
||||
FROM packing_list_items pli
|
||||
LEFT JOIN articles a ON pli.article_id = a.id
|
||||
LEFT JOIN manufacturers m ON a.manufacturer_id = m.id
|
||||
LEFT JOIN backpacks b ON pli.backpack_id = b.id
|
||||
LEFT JOIN backpack_compartments bc ON pli.backpack_compartment_id = bc.id
|
||||
WHERE pli.packing_list_id = ?
|
||||
AND (pli.carrier_user_id IN ($placeholders) OR pli.carrier_user_id IS NULL)
|
||||
ORDER BY pli.order_index ASC";
|
||||
|
||||
$stmt_items = $conn->prepare($sql_items);
|
||||
$params_items = array_merge([$packing_list_id], $household_member_ids);
|
||||
$types_items = 'i' . $types;
|
||||
$stmt_items->bind_param($types_items, ...$params_items);
|
||||
$stmt_items->bind_param("i", $packing_list_id);
|
||||
$stmt_items->execute();
|
||||
$packed_items_raw = $stmt_items->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
$stmt_items->close();
|
||||
|
||||
$carriers_data = [];
|
||||
$stmt_carriers = $conn->prepare("SELECT id, username FROM users WHERE id IN ($placeholders)");
|
||||
$stmt_carriers->bind_param($types, ...$household_member_ids);
|
||||
// KORREKTUR: Nur Träger anzeigen, die dieser Liste zugewiesen sind
|
||||
$stmt_carriers = $conn->prepare("SELECT u.id, u.username FROM users u JOIN packing_list_carriers plc ON u.id = plc.user_id WHERE plc.packing_list_id = ? ORDER BY u.username");
|
||||
$stmt_carriers->bind_param("i", $packing_list_id);
|
||||
$stmt_carriers->execute();
|
||||
$carriers_result = $stmt_carriers->get_result();
|
||||
while ($row = $carriers_result->fetch_assoc()) { $carriers_data[] = $row; }
|
||||
|
||||
@@ -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
101
src/packing_list_utils.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
// src/packing_list_utils.php - Hilfsfunktionen für Packlisten
|
||||
|
||||
/**
|
||||
* Kopiert den gesamten Inhalt einer Packliste (Items, Träger, Hierarchie) in eine neue Liste.
|
||||
*
|
||||
* @param int $sourceId ID der Quell-Packliste
|
||||
* @param int $targetId ID der Ziel-Packliste
|
||||
* @param mysqli $conn Datenbankverbindung
|
||||
* @throws Exception Bei Datenbankfehlern
|
||||
*/
|
||||
function copyPackingListContent($sourceId, $targetId, $conn) {
|
||||
// 1. Träger kopieren (packing_list_carriers)
|
||||
$stmt_carriers = $conn->prepare("SELECT user_id, backpack_id FROM packing_list_carriers WHERE packing_list_id = ?");
|
||||
if (!$stmt_carriers) throw new Exception("Prepare failed for carriers select: " . $conn->error);
|
||||
|
||||
$stmt_carriers->bind_param("i", $sourceId);
|
||||
$stmt_carriers->execute();
|
||||
$result_carriers = $stmt_carriers->get_result();
|
||||
|
||||
$stmt_insert_carrier = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)");
|
||||
if (!$stmt_insert_carrier) throw new Exception("Prepare failed for carriers insert: " . $conn->error);
|
||||
|
||||
while ($row = $result_carriers->fetch_assoc()) {
|
||||
$stmt_insert_carrier->bind_param("iii", $targetId, $row['user_id'], $row['backpack_id']);
|
||||
if (!$stmt_insert_carrier->execute()) throw new Exception("Error inserting carrier: " . $stmt_insert_carrier->error);
|
||||
}
|
||||
$stmt_carriers->close();
|
||||
$stmt_insert_carrier->close();
|
||||
|
||||
// 2. Items kopieren (packing_list_items)
|
||||
// Alle Items inklusive Rucksack/Fach-Info holen
|
||||
$sql_items = "SELECT id, article_id, quantity, parent_packing_list_item_id, carrier_user_id, backpack_id, backpack_compartment_id, name, order_index
|
||||
FROM packing_list_items WHERE packing_list_id = ?";
|
||||
$stmt_items = $conn->prepare($sql_items);
|
||||
if (!$stmt_items) throw new Exception("Prepare failed for items select: " . $conn->error);
|
||||
|
||||
$stmt_items->bind_param("i", $sourceId);
|
||||
$stmt_items->execute();
|
||||
$result_items = $stmt_items->get_result();
|
||||
|
||||
$original_items = [];
|
||||
while ($row = $result_items->fetch_assoc()) {
|
||||
$original_items[] = $row;
|
||||
}
|
||||
$stmt_items->close();
|
||||
|
||||
if (!empty($original_items)) {
|
||||
$old_to_new_id_map = []; // Mapping: alte_ID => neue_ID
|
||||
|
||||
// 2a. Items einfügen (zunächst ohne Parent-Referenz)
|
||||
$sql_insert = "INSERT INTO packing_list_items
|
||||
(packing_list_id, article_id, quantity, carrier_user_id, backpack_id, backpack_compartment_id, name, order_index)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
$stmt_insert_item = $conn->prepare($sql_insert);
|
||||
if (!$stmt_insert_item) throw new Exception("Prepare failed for item insert: " . $conn->error);
|
||||
|
||||
foreach ($original_items as $item) {
|
||||
$stmt_insert_item->bind_param(
|
||||
"iiiisiis",
|
||||
$targetId,
|
||||
$item['article_id'],
|
||||
$item['quantity'],
|
||||
$item['carrier_user_id'],
|
||||
$item['backpack_id'],
|
||||
$item['backpack_compartment_id'],
|
||||
$item['name'],
|
||||
$item['order_index']
|
||||
);
|
||||
|
||||
if (!$stmt_insert_item->execute()) {
|
||||
throw new Exception("Error copying item ID " . $item['article_id'] . ": " . $stmt_insert_item->error);
|
||||
}
|
||||
$old_to_new_id_map[$item['id']] = $conn->insert_id;
|
||||
}
|
||||
$stmt_insert_item->close();
|
||||
|
||||
// 2b. Parent-Referenzen aktualisieren (Hierarchie wiederherstellen)
|
||||
$stmt_update_parent = $conn->prepare("UPDATE packing_list_items SET parent_packing_list_item_id = ? WHERE id = ?");
|
||||
if (!$stmt_update_parent) throw new Exception("Prepare failed for parent update: " . $conn->error);
|
||||
|
||||
foreach ($original_items as $item) {
|
||||
if (!empty($item['parent_packing_list_item_id'])) {
|
||||
$old_parent_id = $item['parent_packing_list_item_id'];
|
||||
|
||||
// Prüfen, ob der Parent auch kopiert wurde (sollte immer so sein)
|
||||
if (isset($old_to_new_id_map[$old_parent_id])) {
|
||||
$new_parent_id = $old_to_new_id_map[$old_parent_id];
|
||||
$new_child_id = $old_to_new_id_map[$item['id']];
|
||||
|
||||
$stmt_update_parent->bind_param("ii", $new_parent_id, $new_child_id);
|
||||
if (!$stmt_update_parent->execute()) {
|
||||
throw new Exception("Error updating parent hierarchy: " . $stmt_update_parent->error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$stmt_update_parent->close();
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
// packing_lists.php - Übersicht über alle Haushalts-Packlisten
|
||||
// KORREKTUR: Bearbeiten-Buttons werden nun auch für Haushaltsmitglieder bei geteilten Listen angezeigt.
|
||||
// packing_lists.php - Übersicht über alle Haushalts-Packlisten und Vorlagen
|
||||
|
||||
$page_title = "Packlisten des Haushalts";
|
||||
|
||||
@@ -16,8 +15,18 @@ require_once 'db_connect.php';
|
||||
require_once 'header.php';
|
||||
|
||||
$current_user_id = $_SESSION['user_id'];
|
||||
$message = '';
|
||||
|
||||
// AUTO-MIGRATION: Check columns to prevent SQL errors
|
||||
$check_col = $conn->query("SHOW COLUMNS FROM packing_lists LIKE 'is_template'");
|
||||
if ($check_col && $check_col->num_rows == 0) {
|
||||
// Add column if missing
|
||||
$conn->query("ALTER TABLE packing_lists ADD COLUMN is_template TINYINT(1) NOT NULL DEFAULT 0 AFTER share_token");
|
||||
}
|
||||
|
||||
$view = isset($_GET['view']) && $_GET['view'] === 'templates' ? 'templates' : 'lists';
|
||||
$is_template_view = ($view === 'templates');
|
||||
|
||||
$message = '';
|
||||
if (isset($_SESSION['message'])) {
|
||||
$message = $_SESSION['message'];
|
||||
unset($_SESSION['message']);
|
||||
@@ -30,25 +39,11 @@ $stmt_household->execute();
|
||||
$current_user_household_id = $stmt_household->get_result()->fetch_assoc()['household_id'];
|
||||
$stmt_household->close();
|
||||
|
||||
$household_member_ids = [$current_user_id];
|
||||
if ($current_user_household_id) {
|
||||
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
|
||||
$stmt_members->bind_param("i", $current_user_household_id);
|
||||
$stmt_members->execute();
|
||||
$result_members = $stmt_members->get_result();
|
||||
while ($row = $result_members->fetch_assoc()) {
|
||||
if (!in_array($row['id'], $household_member_ids)) {
|
||||
$household_member_ids[] = $row['id'];
|
||||
}
|
||||
}
|
||||
$stmt_members->close();
|
||||
}
|
||||
|
||||
$packing_lists = [];
|
||||
// KORREKTUR: Die SQL-Abfrage wurde vereinfacht und korrigiert, um alle relevanten Listen anzuzeigen.
|
||||
// Es werden alle Listen angezeigt, die entweder vom Nutzer erstellt wurden ODER mit seinem Haushalt geteilt sind.
|
||||
$is_template_val = $is_template_view ? 1 : 0;
|
||||
|
||||
$sql = "SELECT
|
||||
pl.id, pl.name, pl.description, pl.user_id, pl.household_id,
|
||||
pl.id, pl.name, pl.description, pl.user_id, pl.household_id, pl.is_template,
|
||||
u.username as creator_name,
|
||||
COUNT(DISTINCT COALESCE(pli.carrier_user_id, 'sonstiges')) AS carrier_count,
|
||||
SUM(pli.quantity * a.weight_grams) AS total_weight
|
||||
@@ -56,15 +51,17 @@ $sql = "SELECT
|
||||
JOIN users u ON pl.user_id = u.id
|
||||
LEFT JOIN packing_list_items pli ON pl.id = pli.packing_list_id
|
||||
LEFT JOIN articles a ON pli.article_id = a.id
|
||||
WHERE pl.user_id = ? OR pl.household_id = ?
|
||||
GROUP BY pl.id, pl.name, pl.description, pl.user_id, u.username, pl.household_id
|
||||
WHERE (pl.user_id = ? OR pl.household_id = ?) AND pl.is_template = ?
|
||||
GROUP BY pl.id, pl.name, pl.description, pl.user_id, u.username, pl.household_id, pl.is_template
|
||||
ORDER BY pl.name ASC";
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
if ($stmt === false) {
|
||||
$message .= '<div class="alert alert-danger" role="alert">SQL Prepare-Fehler: ' . $conn->error . '</div>';
|
||||
} else {
|
||||
$stmt->bind_param("ii", $current_user_id, $current_user_household_id);
|
||||
// Falls household_id NULL ist, nutzen wir 0 (da ID immer > 0)
|
||||
$h_id = $current_user_household_id ?: 0;
|
||||
$stmt->bind_param("iii", $current_user_id, $h_id, $is_template_val);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
@@ -76,28 +73,53 @@ $conn->close();
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="h4 mb-0">Packlisten im Haushalt</h2>
|
||||
<a href="add_packing_list.php" class="btn btn-sm btn-outline-light"><i class="fas fa-plus-circle me-2"></i>Neue Packliste erstellen</a>
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2 class="h4 mb-0"><?php echo $is_template_view ? 'Meine Vorlagen' : 'Packlisten im Haushalt'; ?></h2>
|
||||
<a href="add_packing_list.php" class="btn btn-sm btn-outline-light"><i class="fas fa-plus-circle me-2"></i>Neue Packliste erstellen</a>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs card-header-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo !$is_template_view ? 'active' : ''; ?>" href="packing_lists.php?view=lists">
|
||||
<i class="fas fa-list me-2"></i>Packlisten
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo $is_template_view ? 'active' : ''; ?>" href="packing_lists.php?view=templates">
|
||||
<i class="fas fa-copy me-2"></i>Vorlagen
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<?php if(!empty($message)) echo $message; ?>
|
||||
<?php if (empty($packing_lists)): ?>
|
||||
<div class="alert alert-info text-center" role="alert">
|
||||
Du hast noch keine Packlisten angelegt. <a href="add_packing_list.php" class="alert-link">Erstelle jetzt deine erste!</a>
|
||||
<?php if ($is_template_view): ?>
|
||||
Du hast noch keine Vorlagen. Erstelle eine Packliste und speichere sie als Vorlage.
|
||||
<?php else: ?>
|
||||
Du hast noch keine Packlisten angelegt. <a href="add_packing_list.php" class="alert-link">Erstelle jetzt deine erste!</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-4">
|
||||
<?php foreach ($packing_lists as $list): ?>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card h-100 shadow-sm packing-list-card">
|
||||
<div class="card h-100 shadow-sm packing-list-card <?php echo $is_template_view ? 'border-info' : ''; ?>">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<img src="assets/images/rucksack_icon.png" alt="Packliste" class="card-icon">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<img src="assets/images/rucksack_icon.png" alt="Packliste" class="card-icon" style="opacity: <?php echo $is_template_view ? '0.7' : '1'; ?>;">
|
||||
<?php if ($is_template_view): ?>
|
||||
<span class="badge bg-info text-dark">VORLAGE</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<h5 class="card-title"><?php echo htmlspecialchars($list['name']); ?></h5>
|
||||
<p class="card-text text-muted flex-grow-1 small"><?php echo htmlspecialchars($list['description'] ?: 'Keine Beschreibung vorhanden.'); ?></p>
|
||||
<div class="mt-auto d-flex justify-content-between">
|
||||
<span class="badge bg-secondary creator-badge"><i class="fas fa-user-edit me-1"></i> Erstellt von <?php echo htmlspecialchars($list['creator_name']); ?></span>
|
||||
<span class="badge bg-secondary creator-badge"><i class="fas fa-user-edit me-1"></i> <?php echo htmlspecialchars($list['creator_name']); ?></span>
|
||||
<?php if (!empty($list['household_id'])): ?>
|
||||
<span class="badge bg-success" data-bs-toggle="tooltip" title="Für den Haushalt freigegeben"><i class="fas fa-users"></i></span>
|
||||
<?php else: ?>
|
||||
@@ -111,19 +133,46 @@ $conn->close();
|
||||
<span data-bs-toggle="tooltip" title="Gesamtgewicht"><i class="fas fa-weight-hanging text-muted"></i> <span class="badge bg-success rounded-pill ms-1"><?php echo number_format(($list['total_weight'] ?? 0) / 1000, 2, ',', '.'); ?> kg</span></span>
|
||||
</div>
|
||||
<div>
|
||||
<a href="packing_list_detail.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-success" title="Details ansehen"><i class="fas fa-eye"></i></a>
|
||||
<?php
|
||||
$is_owner = ($list['user_id'] == $current_user_id);
|
||||
$can_edit_household_list = (!empty($list['household_id']) && $list['household_id'] == $current_user_household_id);
|
||||
|
||||
// Actions für Vorlagen und Listen unterscheiden
|
||||
if ($is_template_view) {
|
||||
// VORLAGEN ACTIONS
|
||||
?>
|
||||
<form action="add_packing_list.php" method="POST" class="d-inline">
|
||||
</form>
|
||||
|
||||
<?php if ($is_owner || $can_edit_household_list): ?>
|
||||
<a href="edit_packing_list_details.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-primary" title="Details bearbeiten"><i class="fas fa-pencil-alt"></i></a>
|
||||
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Inhalt bearbeiten"><i class="fas fa-boxes"></i></a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
} else {
|
||||
// LIST ACTIONS (Standard)
|
||||
?>
|
||||
<a href="packing_list_detail.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-success" title="Details ansehen"><i class="fas fa-eye"></i></a>
|
||||
<?php if ($is_owner || $can_edit_household_list): ?>
|
||||
<a href="edit_packing_list_details.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-primary" title="Details bearbeiten"><i class="fas fa-pencil-alt"></i></a>
|
||||
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Artikel verwalten"><i class="fas fa-boxes"></i></a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($is_owner): ?>
|
||||
<!-- Save As Template -->
|
||||
<a href="save_as_template.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-warning" title="Als Vorlage speichern"><i class="fas fa-save"></i></a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
<?php if ($is_owner || $can_edit_household_list): ?>
|
||||
<a href="edit_packing_list_details.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-primary" title="Details bearbeiten"><i class="fas fa-pencil-alt"></i></a>
|
||||
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Artikel verwalten"><i class="fas fa-boxes"></i></a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($is_owner): ?>
|
||||
<a href="share_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-info" title="Teilen"><i class="fas fa-share-alt"></i></a>
|
||||
<a href="duplicate_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-dark" title="Duplizieren" onclick="return confirm('Möchten Sie diese Packliste wirklich duplizieren?');"><i class="fas fa-copy"></i></a>
|
||||
<a href="delete_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-danger ms-2" title="Löschen" onclick="return confirm('Sind Sie sicher, dass Sie diese Packliste löschen möchten?');"><i class="fas fa-trash"></i></a>
|
||||
<?php if (!$is_template_view): ?>
|
||||
<a href="duplicate_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-dark" title="Duplizieren" onclick="return confirm('Möchten Sie diese Packliste wirklich duplizieren?');"><i class="fas fa-copy"></i></a>
|
||||
<?php endif; ?>
|
||||
<a href="delete_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-danger ms-2" title="Löschen" onclick="return confirm('Sind Sie sicher, dass Sie diese <?php echo $is_template_view ? 'Vorlage' : 'Packliste'; ?> löschen möchten?');"><i class="fas fa-trash"></i></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,4 +184,4 @@ $conn->close();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'footer.php'; ?>
|
||||
<?php require_once 'footer.php'; ?>
|
||||
58
src/save_as_template.php
Normal file
58
src/save_as_template.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// src/save_as_template.php - Speichert eine Packliste als Vorlage (Kopie)
|
||||
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once 'db_connect.php';
|
||||
require_once 'packing_list_utils.php';
|
||||
|
||||
$current_user_id = $_SESSION['user_id'];
|
||||
$list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||
|
||||
if ($list_id <= 0) {
|
||||
header("Location: packing_lists.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prüfen, ob Liste existiert und User Zugriff hat
|
||||
$stmt = $conn->prepare("SELECT name, description, household_id FROM packing_lists WHERE id = ? AND (user_id = ? OR household_id = (SELECT household_id FROM users WHERE id = ?))");
|
||||
// Eigentlich darf nur Owner oder Household-Admin kopieren? Lassen wir es für alle mit Lesezugriff zu.
|
||||
$stmt->bind_param("iii", $list_id, $current_user_id, $current_user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
if ($result->num_rows === 0) {
|
||||
$_SESSION['message'] = '<div class="alert alert-danger">Packliste nicht gefunden oder keine Berechtigung.</div>';
|
||||
header("Location: packing_lists.php");
|
||||
exit;
|
||||
}
|
||||
$list = $result->fetch_assoc();
|
||||
$stmt->close();
|
||||
|
||||
$new_name = "Vorlage: " . $list['name'];
|
||||
|
||||
// Neue Liste (Template) anlegen
|
||||
$stmt_insert = $conn->prepare("INSERT INTO packing_lists (user_id, household_id, name, description, is_template) VALUES (?, ?, ?, ?, 1)");
|
||||
$stmt_insert->bind_param("isss", $current_user_id, $list['household_id'], $new_name, $list['description']);
|
||||
|
||||
if ($stmt_insert->execute()) {
|
||||
$new_template_id = $conn->insert_id;
|
||||
try {
|
||||
copyPackingListContent($list_id, $new_template_id, $conn);
|
||||
$_SESSION['message'] = '<div class="alert alert-success">Packliste erfolgreich als Vorlage gespeichert! Du findest sie im Tab "Vorlagen".</div>';
|
||||
} catch (Exception $e) {
|
||||
// Aufräumen bei Fehler wäre gut, aber hier einfach Fehler melden
|
||||
$_SESSION['message'] = '<div class="alert alert-warning">Vorlage erstellt, aber Inhalt konnte nicht vollständig kopiert werden: ' . $e->getMessage() . '</div>';
|
||||
}
|
||||
} else {
|
||||
$_SESSION['message'] = '<div class="alert alert-danger">Fehler beim Erstellen der Vorlage: ' . $conn->error . '</div>';
|
||||
}
|
||||
|
||||
header("Location: packing_lists.php?view=templates");
|
||||
exit;
|
||||
?>
|
||||
Reference in New Issue
Block a user