Rucksack-Feature finalisiert: Management, Zuweisung und Anzeige implementiert

This commit is contained in:
Gemini Agent
2025-12-04 19:55:38 +00:00
parent 17fb54193f
commit eab7de42a4
224 changed files with 1609 additions and 679 deletions

View File

@@ -20,9 +20,11 @@ Eine moderne, webbasierte Anwendung zur Verwaltung von Packlisten für Wanderung
## Funktionen
* **Artikeldatenbank:** Erfasse deine gesamte Ausrüstung mit Bild, Gewicht, Hersteller und Kategorie.
* **Rucksack-Management:** Definiere deine Rucksäcke inkl. Fächern (Deckelfach, Bodenfach, etc.).
* **Intelligente Packlisten:**
* **Drag & Drop Editor:** Ziehe Artikel einfach aus deinem Bestand in die Liste.
* **Verschachtelung (Nesting):** Organisiere deine Liste logisch. Ziehe Socken *in* den Packsack und den Packsack *in* den Rucksack.
* **Container-Logik:** Weise Trägern Rucksäcke zu. Die Fächer erscheinen automatisch als Ordner, in die du packen kannst.
* **Verschachtelung (Nesting):** Organisiere deine Liste logisch (z.B. Erste-Hilfe-Set im Deckelfach).
* **Haushalts-Modus:** Teile deinen Artikelbestand und deine Listen mit Familie oder Freunden.
* **Gewichtskalkulation:** Sieh sofort, wie schwer dein Rucksack ist und wie viel Gewicht auf welche Kategorie (z.B. "Kochen", "Schlafen") entfällt.
* **Druck-Optimierung:** Generiere eine übersichtliche PDF/Druckansicht zum Abhaken.
@@ -38,14 +40,14 @@ Eine moderne, webbasierte Anwendung zur Verwaltung von Packlisten für Wanderung
* MariaDB / MySQL Datenbank
* PHP-Erweiterungen: `mysqli`, `gd` (für Bildbearbeitung)
### Schritt-für-Schritt
### Neuinstallation
1. **Dateien hochladen:**
Kopiere alle Dateien aus diesem Repository in das Web-Verzeichnis deines Servers (z.B. `/var/www/html/packliste`).
2. **Datenbank einrichten:**
* Erstelle eine leere Datenbank (z.B. `packliste`).
* Importiere die Datei `packliste.sql` in diese Datenbank (z.B. via phpMyAdmin oder Konsole).
* Importiere `packliste.sql` und anschließend `mysql_update.sql` (für die neuesten Features).
3. **Konfiguration:**
* Erstelle eine Datei `config.ini` **außerhalb** des öffentlichen Web-Verzeichnisses (z.B. ein Verzeichnis höher als `index.php`).
@@ -58,8 +60,10 @@ Eine moderne, webbasierte Anwendung zur Verwaltung von Packlisten für Wanderung
```
* *Hinweis:* Die Anwendung sucht standardmäßig unter `../config.ini`.
4. **Berechtigungen:**
Stelle sicher, dass der Webserver Schreibrechte auf den Ordner `uploads/` und `uploads/images/` hat.
### Upgrade (Vorhandene Installation)
1. Führe das SQL-Skript `mysql_update.sql` auf deiner Datenbank aus.
2. Überschreibe die PHP-Dateien mit der neuesten Version.
---
@@ -73,66 +77,40 @@ Nach der Registrierung landest du auf dem Dashboard. Die App ist darauf ausgeleg
* **Mitglieder einladen:** Als Admin des Haushalts kannst du andere registrierte Benutzer per Benutzernamen einladen. Diese sehen nach dem Login eine Einladung, die sie annehmen können.
* **Vorteil:** Alle Artikel, die als "Für Haushalt freigegeben" markiert sind, können von allen Mitgliedern in deren Packlisten verwendet werden.
### 2. Ausrüstung verwalten (Artikel)
### 2. Ausrüstung verwalten (Artikel & Rucksäcke)
Unter **"Artikel"** pflegst du deinen Bestand.
* **Artikel:** Pfleg deinen Bestand unter "Artikel". Du kannst Bilder hochladen, eine URL angeben oder Bilder per Copy & Paste einfügen.
* **Rucksäcke:** Unter **"Rucksäcke"** legst du deine Transportmittel an. Definiere hier auch die **Fächer** (z.B. Hauptfach, Deckelfach, Hüftgurt). Diese dienen später als Container in deinen Listen.
* **Erstellen:** Klicke auf "Neuen Artikel hinzufügen".
* **Bilder:** Du kannst Bilder hochladen, eine URL angeben oder (am Desktop sehr praktisch) ein Bild einfach per **Strg+V (Copy & Paste)** in das markierte Feld einfügen.
* **Eigenschaften:**
* **Gewicht:** Sei präzise! Das ist die Basis für die Statistiken.
* **Verbrauchsartikel:** Markiere Dinge wie Gas, Essen oder Sonnencreme. Diese haben in Packlisten standardmäßig die Menge 1, können aber beliebig erhöht werden.
* **Lagerort:** Hilft dir, das Zeug zu Hause wiederzufinden.
* **Listenansicht:** Die Artikelliste ist nach Kategorien gruppiert. Du kannst Kategorien ein- und ausklappen und oben über das Suchfeld blitzschnell filtern (z.B. nach "Zelt" oder "Merino").
### 3. Stammdaten (Kategorien, Orte, Hersteller)
Damit alles ordentlich bleibt, solltest du die Stammdaten pflegen:
* **Kategorien:** Definiere grobe Gruppen wie "Schlafen", "Küche", "Kleidung".
* **Lagerorte:** Hierarchisch aufgebaut (z.B. "Kellerregal" -> "Kiste 3").
* **Hersteller:** Wird oft beim Artikelanlegen automatisch mit angelegt, kann hier aber bereinigt werden.
### 4. Packlisten erstellen & bearbeiten
### 3. Packlisten erstellen & bearbeiten
Dies ist das Herzstück der Anwendung.
1. Gehe zu **"Packlisten"** und erstelle eine neue Liste.
2. Klicke in der Übersicht auf **"Artikel verwalten"** (das Box-Icon) oder "Bearbeiten".
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).
#### Der Packlisten-Editor (Drag & Drop)
Der Editor ist zweigeteilt:
* **Links:** Dein verfügbarer Artikelbestand (filterbar).
* **Rechts:** Deine Packliste, gruppiert nach Trägern (Personen im Haushalt).
* **Rechts:** Deine Packliste, gruppiert nach Trägern und deren Rucksäcken/Fächern.
**Bedienung:**
* **Hinzufügen:** Ziehe einen Artikel von links nach rechts in den Bereich einer Person.
* **Verschachteln (Nesting):** Das Killer-Feature! Du kannst Artikel **in** andere Artikel ziehen.
* *Beispiel:* Ziehe den "Schlafsack" auf den "Rucksack". Der Schlafsack ist nun logisch im Rucksack verpackt.
* Dies hilft enorm, die Übersicht zu behalten und beim Packen strukturiert vorzugehen.
* **Packen:** Ziehe Artikel in die entsprechenden Fächer deines Rucksacks.
* **Verschachteln:** Du kannst Artikel auch in andere Artikel ziehen (z.B. "Socken" in "Packbeutel").
* **Sortieren:** Ziehe Artikel nach oben oder unten, um die Reihenfolge zu ändern.
* **Menge:** Ändere die Anzahl direkt im Eingabefeld.
* **Komponenten:** Wenn du einen Artikel hinzufügst, der selbst Unter-Artikel im Bestand definiert hat (z.B. "Zelt" besteht aus "Zeltplane", "Gestänge", "Heringen"), fragt dich die App, ob diese Komponenten automatisch mit auf die Liste sollen.
### 5. Drucken & Export
### 4. Drucken & Export
Klicke in der Packlisten-Übersicht auf **"Details ansehen"** (Augen-Icon).
* **Ansicht:** Du siehst eine hierarchische Baumstruktur deiner Liste.
* **Statistiken:** Rechts (oder mobil unten) siehst du Diagramme zur Gewichtsverteilung nach Kategorie und Träger.
* **Drucken:** Klicke auf "Drucken". Die Ansicht ist speziell für Papier optimiert:
* Farben und Bilder werden reduziert.
* Checkboxen zum manuellen Abhaken werden eingeblendet.
* Ideal als Checkliste für den Rucksack-Pack-Abend.
* **Ansicht:** Du siehst eine hierarchische Baumstruktur deiner Liste. Rucksäcke sind grün hinterlegt.
* **Drucken:** Klicke auf "Drucken". Die Ansicht ist speziell für Papier optimiert (fettgedruckte Fächer, Checkboxen für Artikel).
### 6. Profil & Einstellungen
### 5. Profil & Einstellungen
Unter **"Profil"** kannst du:
* Dein Passwort und Benutzernamen ändern.
* **App-Einstellungen:**
* *Tabellenanzeige:* Wie viele Items pro Seite?
* *Kategorien:* Sollen in der Artikelübersicht alle Kategorien standardmäßig eingeklappt sein? (Nützlich bei sehr vielen Artikeln).
Unter **"Profil"** kannst du dein Passwort ändern und App-Einstellungen vornehmen.
---

View File

@@ -1,110 +0,0 @@
<?php
// add_packing_list.php - Neue Packliste hinzufügen
// KORREKTUR: Die Verarbeitungslogik wurde an den Anfang der Datei verschoben, um die Weiterleitung zu reparieren.
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 'household_actions.php'; // Für die Logging-Funktion
$current_user_id = $_SESSION['user_id'];
$message = '';
$name = '';
$description = '';
// KORREKTUR: Die gesamte POST-Verarbeitung muss VOR jeglicher HTML-Ausgabe (wie header.php) stattfinden.
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// Lade Haushalts-ID für die Verarbeitung
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$household_id_for_user = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
$name = trim($_POST['name']);
$description = trim($_POST['description']);
$household_id = isset($_POST['is_household_list']) && $household_id_for_user ? $household_id_for_user : NULL;
if (empty($name)) {
$message = '<div class="alert alert-danger" role="alert">Der Packlistenname darf nicht leer sein.</div>';
} else {
$stmt = $conn->prepare("INSERT INTO packing_lists (user_id, household_id, name, description) VALUES (?, ?, ?, ?)");
if ($stmt === false) {
$message .= '<div class="alert alert-danger" role="alert">SQL Prepare-Fehler: ' . $conn->error . '</div>';
} else {
$stmt->bind_param("isss", $current_user_id, $household_id, $name, $description);
if ($stmt->execute()) {
$new_list_id = $conn->insert_id;
if ($household_id_for_user) {
$log_message = htmlspecialchars($_SESSION['username']) . " hat die Packliste '" . htmlspecialchars($name) . "' erstellt.";
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! Du kannst jetzt Artikel hinzufügen.</div>';
header("Location: manage_packing_list_items.php?id=" . $new_list_id);
exit; // exit() ist wichtig nach einer header-Weiterleitung
} else {
$message .= '<div class="alert alert-danger" role="alert">Fehler beim Hinzufügen der Packliste: ' . $stmt->error . '</div>';
}
$stmt->close();
}
}
}
// Die restlichen Daten werden nur für die Anzeige geladen, falls keine Weiterleitung erfolgt.
require_once 'header.php';
$stmt_household_display = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household_display->bind_param("i", $current_user_id);
$stmt_household_display->execute();
$household_id_for_display = $stmt_household_display->get_result()->fetch_assoc()['household_id'];
$stmt_household_display->close();
$conn->close();
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0"><i class="fas fa-plus-circle me-2"></i>Neue Packliste erstellen</h2>
<a href="packing_lists.php" class="btn btn-sm btn-outline-light"><i class="fas fa-arrow-left me-2"></i>Zur Übersicht</a>
</div>
<div class="card-body p-4">
<?php if(!empty($message)) echo $message; ?>
<form action="add_packing_list.php" method="post">
<div class="mb-3">
<label for="name" class="form-label"><i class="fas fa-file-signature me-2 text-muted"></i>Name der Packliste</label>
<input type="text" class="form-control" id="name" value="<?php echo htmlspecialchars($name); ?>" name="name" required>
</div>
<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>
<div class="form-text">Eine kurze Beschreibung, worum es bei dieser Packliste geht.</div>
</div>
<?php if ($household_id_for_display): ?>
<hr>
<div class="form-check form-switch mb-4">
<input class="form-check-input" type="checkbox" role="switch" id="is_household_list" name="is_household_list" value="1">
<label class="form-check-label" for="is_household_list">Für den gesamten Haushalt freigeben (kann von allen Mitgliedern bearbeitet werden)</label>
</div>
<?php endif; ?>
<hr>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary"><i class="fas fa-plus-circle me-2"></i>Packliste erstellen & Artikel hinzufügen</button>
<a href="packing_lists.php" class="btn btn-secondary"><i class="fas fa-arrow-left me-2"></i>Abbrechen</a>
</div>
</form>
</div>
</div>
<?php require_once 'footer.php'; ?>

View File

@@ -1,133 +0,0 @@
<?php
// edit_packing_list_details.php - Seite zum Bearbeiten von Name und Beschreibung einer Packliste
// KORREKTUR: Haushaltsmitglieder dürfen Haushaltslisten bearbeiten.
$page_title = "Packliste Details bearbeiten";
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 'household_actions.php'; // Für die Logging-Funktion
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$message = '';
$packing_list = null;
$can_edit = false;
if ($packing_list_id > 0) {
$stmt_household_check = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household_check->bind_param("i", $current_user_id);
$stmt_household_check->execute();
$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->bind_param("i", $packing_list_id);
$stmt_list_check->execute();
$result = $stmt_list_check->get_result();
if ($result->num_rows == 1) {
$packing_list = $result->fetch_assoc();
$is_owner = ($packing_list['user_id'] == $current_user_id);
$is_household_list = !empty($packing_list['household_id']);
$is_in_same_household = ($is_household_list && $packing_list['household_id'] == $current_user_household_id);
if ($is_owner || $is_in_same_household) {
$can_edit = true;
} else {
$message = '<div class="alert alert-danger" role="alert">Sie sind nicht berechtigt, diese Packliste zu bearbeiten.</div>';
$packing_list = null;
}
} else {
$message = '<div class="alert alert-warning" role="alert">Packliste nicht gefunden.</div>';
}
$stmt_list_check->close();
} else {
$message = '<div class="alert alert-danger" role="alert">Keine Packlisten-ID zum Bearbeiten angegeben.</div>';
}
if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
$name = trim($_POST['name']);
$description = trim($_POST['description']);
if (empty($name)) {
$message = '<div class="alert alert-danger" role="alert">Bitte geben Sie einen Namen für die Packliste an.</div>';
} else {
$stmt_update = $conn->prepare("UPDATE packing_lists SET name = ?, description = ? WHERE id = ?");
if ($stmt_update) {
$stmt_update->bind_param("ssi", $name, $description, $packing_list_id);
if ($stmt_update->execute()) {
if ($stmt_update->affected_rows > 0) {
if ($current_user_household_id) {
$log_message = htmlspecialchars($_SESSION['username']) . " hat die Packliste '" . htmlspecialchars($name) . "' bearbeitet.";
log_household_action($conn, $current_user_household_id, $current_user_id, $log_message);
}
$message = '<div class="alert alert-success" role="alert">Packliste erfolgreich aktualisiert!</div>';
$packing_list['name'] = $name;
$packing_list['description'] = $description;
} else {
$message = '<div class="alert alert-info" role="alert">Keine Änderungen vorgenommen.</div>';
}
} else {
$message = '<div class="alert alert-danger" role="alert">Fehler beim Aktualisieren der Packliste: ' . $stmt_update->error . '</div>';
}
$stmt_update->close();
}
}
}
$conn->close();
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Details bearbeiten: <?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 Übersicht</a>
</div>
<div class="card-body p-4">
<?php echo $message; ?>
<?php if ($packing_list): ?>
<form action="edit_packing_list_details.php?id=<?php echo htmlspecialchars($packing_list_id); ?>" method="post">
<div class="mb-3">
<label for="name" class="form-label"><i class="fas fa-file-signature me-2 text-muted"></i>Name der Packliste</label>
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($packing_list['name']); ?>" required>
</div>
<div class="mb-3">
<label for="description" class="form-label"><i class="fas fa-align-left me-2 text-muted"></i>Beschreibung</label>
<textarea class="form-control" id="description" name="description" rows="4"><?php echo htmlspecialchars($packing_list['description'] ?: ''); ?></textarea>
<div class="form-text">
Eine kurze Beschreibung oder Notizen zu dieser Packliste.
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center">
<button type="submit" class="btn btn-primary"><i class="fas fa-save me-2"></i>Änderungen speichern</button>
<div>
<a href="manage_packing_list_items.php?id=<?php echo htmlspecialchars($packing_list_id); ?>" class="btn btn-info">
<i class="fas fa-boxes me-2"></i>Artikel verwalten
</a>
<a href="packing_list_detail.php?id=<?php echo htmlspecialchars($packing_list_id); ?>" class="btn btn-secondary ms-2">
<i class="fas fa-eye me-2"></i>Zurück zur Ansicht
</a>
</div>
</div>
</form>
<?php elseif(empty($message)): ?>
<div class="alert alert-info text-center" role="alert">
Es gab ein Problem beim Laden der Packliste.
<br><a href="packing_lists.php" class="alert-link mt-2 d-inline-block">Zurück zur Packlisten-Übersicht</a>
</div>
<?php endif; ?>
</div>
</div>
<?php require_once 'footer.php'; ?>

51
mysql_update.sql Normal file
View File

@@ -0,0 +1,51 @@
-- Datenbank-Update: Rucksack-Feature
-- Führe dieses Skript aus, um eine bestehende Installation zu aktualisieren.
-- 1. Neue Tabelle für Rucksäcke
CREATE TABLE IF NOT EXISTS backpacks (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
household_id INT DEFAULT NULL,
name VARCHAR(255) NOT NULL,
manufacturer VARCHAR(255),
model VARCHAR(255),
weight_grams INT DEFAULT 0,
volume_liters INT DEFAULT 0,
image_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_backpack_name (user_id, household_id, name)
);
-- 2. Neue Tabelle für Rucksack-Fächer
CREATE TABLE IF NOT EXISTS backpack_compartments (
id INT AUTO_INCREMENT PRIMARY KEY,
backpack_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
sort_order INT DEFAULT 0,
FOREIGN KEY (backpack_id) REFERENCES backpacks(id) ON DELETE CASCADE
);
-- 3. Neue Tabelle für die Zuweisung von Rucksäcken zu Trägern pro Liste
CREATE TABLE IF NOT EXISTS packing_list_carriers (
id INT AUTO_INCREMENT PRIMARY KEY,
packing_list_id INT NOT NULL,
user_id INT NOT NULL,
backpack_id INT DEFAULT NULL,
FOREIGN KEY (packing_list_id) REFERENCES packing_lists(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (backpack_id) REFERENCES backpacks(id) ON DELETE SET NULL
);
-- 4. Anpassung der packing_list_items Tabelle
-- Hinzufügen der Spalten für Rucksack-Referenzen und freie Namen (für Container)
ALTER TABLE packing_list_items ADD COLUMN IF NOT EXISTS backpack_id INT DEFAULT NULL;
ALTER TABLE packing_list_items ADD COLUMN IF NOT EXISTS backpack_compartment_id INT DEFAULT NULL;
ALTER TABLE packing_list_items ADD COLUMN IF NOT EXISTS name VARCHAR(255) DEFAULT NULL;
-- Foreign Keys setzen
ALTER TABLE packing_list_items ADD CONSTRAINT fk_pli_backpack FOREIGN KEY (backpack_id) REFERENCES backpacks(id) ON DELETE SET NULL;
ALTER TABLE packing_list_items ADD CONSTRAINT fk_pli_compartment FOREIGN KEY (backpack_compartment_id) REFERENCES backpack_compartments(id) ON DELETE SET NULL;
-- WICHTIG: article_id muss NULL sein dürfen (für reine Container-Items)
ALTER TABLE packing_list_items MODIFY COLUMN article_id INT NULL;

View File

@@ -1,361 +0,0 @@
<?php
// packing_list_detail.php - Detailansicht einer Packliste
// FINALE, STABILE VERSION: Mit modernem Tree-View, Toggle-Funktion und externem CSS.
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 'header.php';
$current_user_id = $_SESSION['user_id'];
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$packing_list = null;
$all_items = [];
$weight_by_category = [];
$weight_by_carrier = [];
$weight_by_carrier_non_consumable = [];
$total_weight_grams = 0;
$total_consumable_weight = 0;
if ($packing_list_id <= 0) {
die("Keine Packlisten-ID angegeben.");
}
// Erweiterte Berechtigungsprüfung für Haushalte
$stmt_current_user = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_current_user->bind_param("i", $current_user_id);
$stmt_current_user->execute();
$current_user_household_id = $stmt_current_user->get_result()->fetch_assoc()['household_id'];
$stmt_current_user->close();
$stmt_list_owner = $conn->prepare(
"SELECT pl.*, u.household_id as owner_household_id
FROM packing_lists pl
JOIN users u ON pl.user_id = u.id
WHERE pl.id = ?"
);
$stmt_list_owner->bind_param("i", $packing_list_id);
$stmt_list_owner->execute();
$result = $stmt_list_owner->get_result();
if ($result->num_rows > 0) {
$packing_list = $result->fetch_assoc();
$owner_household_id = $packing_list['owner_household_id'];
$is_owner = ($packing_list['user_id'] == $current_user_id);
$is_household_list = !empty($packing_list['household_id']);
$is_in_same_household = ($is_household_list && $packing_list['household_id'] == $current_user_household_id);
if (!$is_owner && !$is_in_same_household) {
die("Packliste nicht gefunden oder Zugriff verweigert.");
}
} else {
die("Packliste nicht gefunden oder Zugriff verweigert.");
}
$stmt_list_owner->close();
$page_title = "Packliste: " . htmlspecialchars($packing_list['name']);
$sql = "SELECT pli.id, pli.quantity, pli.parent_packing_list_item_id, a.name AS article_name, a.weight_grams, a.image_url, a.product_designation, a.consumable, c.name AS category_name, m.name AS manufacturer_name, u.username AS carrier_name, l2.name AS location_level2_name, l1.name AS location_level1_name FROM packing_list_items AS pli JOIN articles AS a ON pli.article_id = a.id LEFT JOIN categories AS c ON a.category_id = c.id LEFT JOIN manufacturers AS m ON a.manufacturer_id = m.id LEFT JOIN users AS u ON pli.carrier_user_id = u.id LEFT JOIN storage_locations l2 ON a.storage_location_id = l2.id LEFT JOIN storage_locations l1 ON l2.parent_id = l1.id WHERE pli.packing_list_id = ?";
$stmt = $conn->prepare($sql);
if ($stmt === false) { die('SQL-Vorbereitung fehlgeschlagen: ' . htmlspecialchars($conn->error)); }
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
$all_items[$row['id']] = $row;
$item_total_weight = $row['quantity'] * $row['weight_grams'];
$total_weight_grams += $item_total_weight;
$category_name = $row['category_name'] ?: 'Ohne Kategorie';
$carrier_name = $row['carrier_name'] ?: 'Sonstiges';
@$weight_by_category[$category_name] += $item_total_weight;
@$weight_by_carrier[$carrier_name] += $item_total_weight;
if ($row['consumable']) {
$total_consumable_weight += $item_total_weight;
} else {
@$weight_by_carrier_non_consumable[$carrier_name] += $item_total_weight;
}
}
$stmt->close();
$conn->close();
$total_weight_without_consumables = $total_weight_grams - $total_consumable_weight;
$items_by_carrier_hierarchical = [];
foreach($all_items as $item) {
$carrier_name = $item['carrier_name'] ?: 'Sonstiges';
$items_by_carrier_hierarchical[$carrier_name][$item['id']] = $item;
}
foreach($items_by_carrier_hierarchical as &$carrier_items) {
$hierarchical_list = [];
foreach ($carrier_items as $id => &$item) {
if (!isset($item['children'])) {
$item['children'] = [];
}
if (empty($item['parent_packing_list_item_id']) || !isset($carrier_items[$item['parent_packing_list_item_id']])) {
$hierarchical_list[$id] = &$item;
} else {
$carrier_items[$item['parent_packing_list_item_id']]['children'][] = &$item;
}
}
$carrier_items = $hierarchical_list;
}
unset($carrier_items, $item);
$sorted_weight_by_carrier = [];
$temp_carrier_weights = $weight_by_carrier;
$current_username = $_SESSION['username'];
if (isset($temp_carrier_weights[$current_username])) {
$sorted_weight_by_carrier[$current_username] = $temp_carrier_weights[$current_username];
unset($temp_carrier_weights[$current_username]);
}
$unassigned_weight = null;
if (isset($temp_carrier_weights['Sonstiges'])) {
$unassigned_weight = $temp_carrier_weights['Sonstiges'];
unset($temp_carrier_weights['Sonstiges']);
}
ksort($temp_carrier_weights);
$sorted_weight_by_carrier += $temp_carrier_weights;
if ($unassigned_weight !== null) {
$sorted_weight_by_carrier['Sonstiges'] = $unassigned_weight;
}
$sorted_items_by_carrier = array_replace(array_flip(array_keys($sorted_weight_by_carrier)), $items_by_carrier_hierarchical);
function render_screen_rows($items, $level = 0, $parent_id = null) {
foreach ($items as $item) {
$item_weight = $item['quantity'] * $item['weight_grams'];
$has_children = !empty($item['children']);
$image_path = !empty($item['image_url']) ? htmlspecialchars($item['image_url']) : 'keinbild.png';
$row_class = 'level-' . $level;
$parent_attr = $parent_id ? 'data-parent-id="' . $parent_id . '"' : '';
echo '<tr class="align-middle tree-row" data-id="' . $item['id'] . '" ' . $parent_attr . ' data-level="' . $level . '">';
echo '<td><img src="' . $image_path . '" alt="' . htmlspecialchars($item['article_name']) . '" class="item-image article-image-trigger" data-preview-url="' . $image_path . '"></td>';
echo '<td>';
echo '<div class="tree-view-item ' . $row_class . '">';
echo '<span class="tree-line"></span>';
if ($has_children) {
echo '<button class="btn btn-sm btn-link p-0 me-2 text-decoration-none toggle-tree-btn" data-target-id="' . $item['id'] . '"><i class="fas fa-chevron-down"></i></button>';
} else {
echo '<span class="d-inline-block" style="width: 18px; margin-right: 0.5rem;"></span>';
}
echo '<span class="item-name-text">' . htmlspecialchars($item['article_name']) . '</span>';
echo '</div>';
echo '</td>';
echo '<td class="text-center">' . ($item['consumable'] ? '<i class="fas fa-cookie-bite text-warning" data-bs-toggle="tooltip" title="Verbrauchsartikel"></i>' : '') . '</td>';
echo '<td>' . htmlspecialchars($item['manufacturer_name'] ?: '---') . '</td>';
echo '<td>' . htmlspecialchars($item['product_designation'] ?: '---') . '</td>';
echo '<td>' . htmlspecialchars($item['category_name'] ?: '---') . '</td>';
echo '<td class="text-center"><span class="badge bg-secondary rounded-pill">' . $item['quantity'] . 'x</span></td>';
echo '<td class="text-end"><span class="text-muted small">' . number_format($item['weight_grams'], 0, ',', '.') . ' g</span></td>';
echo '<td class="text-end fw-bold">' . number_format($item_weight, 0, ',', '.') . ' g</td>';
echo '</tr>';
if ($has_children) render_screen_rows($item['children'], $level + 1, $item['id']);
}
}
function render_print_table_rows($items, $level = 0) {
foreach($items as $item) {
$location_string = !empty($item['location_level1_name']) ? $item['location_level1_name'] . ' > ' . $item['location_level2_name'] : '---';
echo '<tr class="' . ($level > 0 ? 'print-child-item' : '') . '">';
echo '<td class="print-checkbox-cell"><span class="print-checkbox"></span></td>';
echo '<td><div style="padding-left: ' . ($level * 15) . 'px;">' . ($level > 0 ? '<span class="arrow-wrapper"></span>' : '') . htmlspecialchars($item['article_name']) . '</div></td>';
echo '<td>' . htmlspecialchars($item['manufacturer_name'] ?: '---') . '</td>';
echo '<td>' . htmlspecialchars($item['product_designation'] ?: '---') . '</td>';
echo '<td>' . htmlspecialchars($location_string) . '</td>';
echo '<td class="text-center">' . $item['quantity'] . 'x</td>';
echo '</tr>';
if (!empty($item['children'])) {
render_print_table_rows($item['children'], $level + 1);
}
}
}
?>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
<div class="screen-view">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h1 class="h4 mb-0"><i class="fas fa-clipboard-list me-2"></i>Packliste: <?php echo htmlspecialchars($packing_list['name']); ?></h1>
<div class="print-hide">
<?php if ($packing_list['user_id'] == $current_user_id || (!empty($packing_list['household_id']) && $packing_list['household_id'] == $current_user_household_id)): ?>
<a href="manage_packing_list_items.php?id=<?php echo $packing_list_id; ?>" class="btn btn-light btn-sm"><i class="fas fa-edit me-2"></i>Bearbeiten</a>
<?php endif; ?>
<a href="packing_lists.php" class="btn btn-light btn-sm ms-2"><i class="fas fa-arrow-left me-2"></i>Zur Übersicht</a>
<button onclick="window.print();" class="btn btn-light btn-sm ms-2"><i class="fas fa-print me-2"></i>Drucken</button>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 mb-4 mb-lg-0">
<div class="card h-100">
<div class="card-header"><h5 class="mb-0"><i class="fas fa-box-open me-2"></i>Gepackte Artikel</h5></div>
<div class="card-body p-2">
<div class="table-responsive">
<table class="table table-hover table-sm mb-0">
<thead><tr><th style="width: 50px;">Bild</th><th>Artikel</th><th class="text-center" data-bs-toggle="tooltip" title="Verbrauchsartikel"><i class="fas fa-cookie-bite"></i></th><th>Hersteller</th><th>Modell/Typ</th><th>Kategorie</th><th class="text-center">Menge</th><th class="text-end">Gewicht/Stk.</th><th class="text-end">G-Gewicht</th></tr></thead>
<tbody>
<?php if (empty($sorted_items_by_carrier)): ?>
<tr><td colspan="9" class="text-center p-4">Diese Packliste ist leer.</td></tr>
<?php else: ?>
<?php $is_first = true; foreach($sorted_items_by_carrier as $carrier => $items): ?>
<?php if (!$is_first): ?>
<!-- Spacer Row for visual separation between carriers -->
<tr class="spacer-row"><td colspan="9" style="height: 2rem; background-color: transparent; border: none;"></td></tr>
<?php endif; ?>
<tr class="carrier-header-row"><td colspan="9"><h6><?php echo htmlspecialchars($carrier); ?></h6></td></tr>
<?php render_screen_rows($items); ?>
<?php $is_first = false; ?>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header card-header-stats"><h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>Statistiken</h5></div>
<div class="card-body">
<div class="stats-table-container mb-4">
<h6>Gewicht pro Träger</h6>
<ul class="list-group list-group-flush bg-transparent">
<?php foreach ($sorted_weight_by_carrier as $carrier => $weight): ?>
<li class="list-group-item px-1 py-2 bg-transparent"><strong><?php echo htmlspecialchars($carrier); ?></strong><div class="d-flex justify-content-between align-items-center text-muted small mt-1">Gesamt:<span class="badge bg-success rounded-pill"><?php echo number_format($weight, 0, ',', '.'); ?> g</span></div><div class="d-flex justify-content-between align-items-center text-muted small">Basisgewicht:<span class="badge bg-secondary rounded-pill"><?php echo number_format($weight_by_carrier_non_consumable[$carrier] ?? 0, 0, ',', '.'); ?> g</span></div></li>
<?php endforeach; ?>
</ul>
</div>
<div class="row">
<div class="col-6"><h6 class="text-center small">nach Kategorie</h6><div class="chart-container"><canvas id="categoryWeightChart"></canvas></div></div>
<div class="col-6"><h6 class="text-center small">nach Träger</h6><div class="chart-container"><canvas id="carrierWeightChart"></canvas></div></div>
</div>
<div class="stats-table-container" id="category-weight-table-container">
<div class="table-responsive"><table id="category-weight-table" class="table table-sm table-hover mb-0"><tbody><?php arsort($weight_by_category); $i = 0; foreach($weight_by_category as $cat => $w): ?><tr data-category-index="<?php echo $i++; ?>"><td><?php echo htmlspecialchars($cat); ?></td><td class="text-end"><?php echo number_format($w, 0, ',', '.'); ?> g</td></tr><?php endforeach; ?></tbody></table></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="print-view">
<div class="print-header">
<h1><?php echo htmlspecialchars($packing_list['name']); ?></h1>
<p><?php echo htmlspecialchars($packing_list['description'] ?: 'Keine Beschreibung'); ?></p>
</div>
<?php if (empty($sorted_items_by_carrier)): ?>
<p>Diese Packliste enthält noch keine Artikel.</p>
<?php else: ?>
<?php foreach ($sorted_items_by_carrier as $carrier_name => $items): ?>
<div class="print-carrier-group">
<h2 class="print-carrier-header"><?php echo htmlspecialchars($carrier_name); ?></h2>
<table class="print-table">
<thead><tr><th class="print-checkbox-cell"></th><th>Artikel</th><th>Hersteller</th><th>Modell/Typ</th><th>Lagerort</th><th class="text-center" style="width: 10%;">Menge</th></tr></thead>
<tbody><?php render_print_table_rows($items); ?></tbody>
</table>
</div>
<?php endforeach; ?>
<?php endif; ?>
<div class="statistics-section">
<h2 class="print-section-title">Statistiken</h2>
<ul class="statistics-list">
<li><strong>Gesamtgewicht:</strong><span><?php echo number_format($total_weight_grams, 0, ',', '.'); ?>g</span></li>
<li><strong>Basisgewicht (ohne Verbrauch):</strong><span><?php echo number_format($total_weight_without_consumables, 0, ',', '.'); ?>g</span></li>
<?php arsort($weight_by_category); foreach ($weight_by_category as $category_name => $weight) : ?>
<li><span>Gewicht <?php echo htmlspecialchars($category_name); ?>:</span><span><?php echo number_format($weight, 0, ',', '.'); ?>g</span></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<div id="image-preview-tooltip"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Tooltip logic
const tooltip = document.getElementById('image-preview-tooltip');
if (tooltip) {
document.querySelectorAll('.article-image-trigger').forEach(trigger => {
trigger.addEventListener('mouseover', e => {
const url = e.target.getAttribute('data-preview-url');
if (url && !url.endsWith('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'; });
});
}
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) });
// Chart logic
Chart.register(ChartDataLabels);
const highContrastColors = ['#00429d', '#4771b2', '#73a2c6', '#a4d3d9', '#deeedb', '#ffc993', '#f48f65', '#d45039', '#930000'];
const catCtx = document.getElementById('categoryWeightChart');
if (catCtx) {
new Chart(catCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(array_keys($weight_by_category)); ?>, datasets: [{ data: <?php echo json_encode(array_values($weight_by_category)); ?>, backgroundColor: highContrastColors, borderWidth: 2, hoverOffset: 15 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } } } });
}
const carrierCtx = document.getElementById('carrierWeightChart');
if (carrierCtx) {
new Chart(carrierCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(array_keys($sorted_weight_by_carrier)); ?>, datasets: [{ data: <?php echo json_encode(array_values($sorted_weight_by_carrier)); ?>, backgroundColor: highContrastColors, borderWidth: 2, hoverOffset: 15 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } } } });
}
// Collapsible Tree Logic
document.querySelectorAll('.toggle-tree-btn').forEach(btn => {
btn.addEventListener('click', function() {
const parentId = this.getAttribute('data-target-id');
const icon = this.querySelector('i');
const isExpanded = icon.classList.contains('fa-chevron-down');
// Toggle icon
icon.classList.toggle('fa-chevron-down');
icon.classList.toggle('fa-chevron-right');
// Find all children (recursive)
toggleChildren(parentId, !isExpanded);
});
});
function toggleChildren(parentId, show) {
// Find all rows that have this parentId
const directChildren = document.querySelectorAll(`tr[data-parent-id="${parentId}"]`);
directChildren.forEach(row => {
row.style.display = show ? '' : 'none';
const rowId = row.getAttribute('data-id');
// If we are hiding, we hide ALL descendants recursively.
// If we are showing, we only show direct children IF their parent (current row) was expanded.
if (!show) {
toggleChildren(rowId, false);
} else {
const btn = row.querySelector('.toggle-tree-btn');
if (btn) {
const isExpanded = btn.querySelector('i').classList.contains('fa-chevron-down');
if (isExpanded) {
toggleChildren(rowId, true);
}
}
}
});
}
});
</script>
<?php require_once 'footer.php'; ?>

45
setup_backpacks.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
require_once 'src/db_connect.php';
$sql_statements = [
"CREATE TABLE IF NOT EXISTS backpacks (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
household_id INT DEFAULT NULL,
name VARCHAR(255) NOT NULL,
manufacturer VARCHAR(255),
model VARCHAR(255),
weight_grams INT DEFAULT 0,
volume_liters INT DEFAULT 0,
image_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)",
"CREATE TABLE IF NOT EXISTS backpack_compartments (
id INT AUTO_INCREMENT PRIMARY KEY,
backpack_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
sort_order INT DEFAULT 0,
FOREIGN KEY (backpack_id) REFERENCES backpacks(id) ON DELETE CASCADE
)",
"CREATE TABLE IF NOT EXISTS packing_list_carriers (
id INT AUTO_INCREMENT PRIMARY KEY,
packing_list_id INT NOT NULL,
user_id INT NOT NULL,
backpack_id INT DEFAULT NULL,
FOREIGN KEY (packing_list_id) REFERENCES packing_lists(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (backpack_id) REFERENCES backpacks(id) ON DELETE SET NULL
)"
];
foreach ($sql_statements as $sql) {
if ($conn->query($sql) === TRUE) {
echo "Table created successfully\n";
} else {
echo "Error creating table: " . $conn->error . "\n";
}
}
$conn->close();
?>

0
add_article.php → src/add_article.php Normal file → Executable file
View File

192
src/add_packing_list.php Executable file
View File

@@ -0,0 +1,192 @@
<?php
// add_packing_list.php - Neue Packliste hinzufügen
// KORREKTUR: Die Verarbeitungslogik wurde an den Anfang der Datei verschoben, um die Weiterleitung zu reparieren.
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 'household_actions.php';
require_once 'backpack_utils.php'; // Shared backpack functions
$current_user_id = $_SESSION['user_id'];
$message = '';
$name = '';
$description = '';
// Fetch data for UI (Household & Users)
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$household_id_for_user = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
// Fetch Users for Backpack Assignment UI
$available_users = [];
if ($household_id_for_user) {
$stmt_u = $conn->prepare("SELECT id, username FROM users WHERE household_id = ?");
$stmt_u->bind_param("i", $household_id_for_user);
} else {
// Just the current user
$stmt_u = $conn->prepare("SELECT id, username FROM users WHERE id = ?");
$stmt_u->bind_param("i", $current_user_id);
}
$stmt_u->execute();
$res_u = $stmt_u->get_result();
while ($r = $res_u->fetch_assoc()) $available_users[] = $r;
$stmt_u->close();
// Handle Form Submission
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$name = trim($_POST['name']);
$description = trim($_POST['description']);
$household_id = isset($_POST['is_household_list']) && $household_id_for_user ? $household_id_for_user : NULL;
if (empty($name)) {
$message = '<div class="alert alert-danger" role="alert">Der Packlistenname darf nicht leer sein.</div>';
} else {
$stmt = $conn->prepare("INSERT INTO packing_lists (user_id, household_id, name, description) VALUES (?, ?, ?, ?)");
if ($stmt === false) {
$message .= '<div class="alert alert-danger" role="alert">SQL Prepare-Fehler: ' . $conn->error . '</div>';
} else {
$stmt->bind_param("isss", $current_user_id, $household_id, $name, $description);
if ($stmt->execute()) {
$new_list_id = $conn->insert_id;
// Handle Backpacks
if (isset($_POST['backpacks'])) {
foreach ($_POST['backpacks'] as $uid => $bid) {
$uid = intval($uid);
$bid = intval($bid);
if ($bid > 0) {
$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);
$stmt_in->execute();
$stmt_in->close();
sync_backpack_items($conn, $new_list_id, $uid, $bid);
}
}
}
if ($household_id_for_user) {
$log_message = htmlspecialchars($_SESSION['username']) . " hat die Packliste '" . htmlspecialchars($name) . "' erstellt.";
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 {
$message .= '<div class="alert alert-danger" role="alert">Fehler beim Hinzufügen der Packliste: ' . $stmt->error . '</div>';
}
$stmt->close();
}
}
}
require_once 'header.php';
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0"><i class="fas fa-plus-circle me-2"></i>Neue Packliste erstellen</h2>
<a href="packing_lists.php" class="btn btn-sm btn-outline-light"><i class="fas fa-arrow-left me-2"></i>Zur Übersicht</a>
</div>
<div class="card-body p-4">
<?php if(!empty($message)) echo $message; ?>
<form action="add_packing_list.php" method="post">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="name" class="form-label"><i class="fas fa-file-signature me-2 text-muted"></i>Name der Packliste</label>
<input type="text" class="form-control" id="name" value="<?php echo htmlspecialchars($name); ?>" name="name" required>
</div>
<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>
<div class="form-text">Eine kurze Beschreibung, worum es bei dieser Packliste geht.</div>
</div>
<?php if ($household_id_for_user): ?>
<div class="form-check form-switch mb-4">
<input class="form-check-input" type="checkbox" role="switch" id="is_household_list" name="is_household_list" value="1" 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"><i class="fas fa-hiking me-2 text-muted"></i>Rucksack-Zuweisung</h5>
<div class="card bg-light border-0">
<div class="card-body">
<p class="small text-muted">Wähle gleich hier, wer welchen Rucksack trägt.</p>
<?php foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $household_id_for_user);
?>
<div class="mb-3">
<label class="form-label fw-bold small"><?php echo htmlspecialchars($user['username']); ?></label>
<select class="form-select form-select-sm" name="backpacks[<?php echo $user['id']; ?>]">
<option value="0">-- Kein Rucksack --</option>
<?php foreach ($user_backpacks as $bp): ?>
<option value="<?php echo $bp['id']; ?>">
<?php echo htmlspecialchars($bp['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary"><i class="fas fa-plus-circle me-2"></i>Packliste erstellen & Artikel hinzufügen</button>
<a href="packing_lists.php" class="btn btn-secondary"><i class="fas fa-arrow-left me-2"></i>Abbrechen</a>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const selects = document.querySelectorAll('select[name^="backpacks"]');
function updateOptions() {
const selectedValues = Array.from(selects)
.map(s => s.value)
.filter(v => v !== "0");
selects.forEach(select => {
const currentVal = select.value;
Array.from(select.options).forEach(option => {
if (option.value === "0") return;
// If this option is selected in another dropdown, hide it
// Unless it's the currently selected value of THIS dropdown
if (selectedValues.includes(option.value) && option.value !== currentVal) {
option.style.display = 'none';
option.disabled = true; // For robust blocking
} else {
option.style.display = '';
option.disabled = false;
}
});
});
}
selects.forEach(s => s.addEventListener('change', updateOptions));
updateOptions(); // Init on load
});
</script>
</div>
<?php require_once 'footer.php'; ?>

View File

@@ -69,7 +69,7 @@ try {
if(!$stmt_delete->execute()) throw new Exception("Fehler beim Löschen der alten Liste.");
$stmt_delete->close();
$stmt_insert = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, article_id, quantity, carrier_user_id, order_index, parent_packing_list_item_id) VALUES (?, ?, ?, ?, ?, ?)");
$stmt_insert = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, article_id, quantity, carrier_user_id, order_index, parent_packing_list_item_id, backpack_id, backpack_compartment_id, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$id_map = []; // Mappt temporäre Frontend-IDs auf neue DB-IDs
foreach ($items_from_frontend as $index => $item_data) {
@@ -77,6 +77,26 @@ try {
$article_id = intval($item_data['article_id']);
$carrier_id = ($item_data['carrier_id'] === 'null') ? NULL : intval($item_data['carrier_id']);
// New fields
$backpack_id = !empty($item_data['backpack_id']) ? intval($item_data['backpack_id']) : NULL;
$backpack_compartment_id = !empty($item_data['backpack_compartment_id']) ? intval($item_data['backpack_compartment_id']) : NULL;
// For normal items, name is NULL (fetched from articles). For containers, name might be set if needed, but usually containers are created via edit_packing_list_details.
// If this sync receives items that are containers (backpack_id set), we might not have 'name' in payload if it came from DOM.
// Actually, the containers are created in 'edit_details'. Sync just re-orders them.
// We need to preserve the 'name' if it exists in the old record, or pass it from frontend.
// Simplified: If article_id is 0 or null, we might need a name.
// BUT: The frontend sends 'article_id' from dataset. Containers created in backend have article_id=0 (or NULL).
// Let's assume for now we just persist what we get. If article_id is missing, we rely on DB to handle it (it's nullable?).
// Wait, the previous INSERT required article_id. We need to change article_id column to be NULLable or handle 0.
$article_id_val = ($article_id > 0) ? $article_id : NULL;
$name_val = NULL; // Only needed if we were passing names from frontend, which we aren't really for existing items.
// Re-fetch name for containers if we are re-inserting them?
// The sync deletes and re-inserts. If we don't provide a name for a container (which has no article_id), it will be lost!
// We MUST fetch the name from the frontend payload or the old DB record.
// Let's adding 'name' to payload in frontend is best.
// WICHTIG: Parent-ID muss bereits gemappt sein. Das Frontend MUSS Eltern VOR Kindern senden.
// Die rekursive "traverse"-Funktion im JS garantiert dies (Pre-Order Traversal).
$parent_pli_id = isset($item_data['parent_pli_id']) && isset($id_map[$item_data['parent_pli_id']]) ? $id_map[$item_data['parent_pli_id']] : null;
@@ -84,7 +104,7 @@ try {
$quantity = isset($old_quantities[$pli_id_frontend]) ? intval($old_quantities[$pli_id_frontend]) : 1;
if ($quantity < 1) $quantity = 1;
$stmt_insert->bind_param("iiiiii", $packing_list_id, $article_id, $quantity, $carrier_id, $index, $parent_pli_id);
$stmt_insert->bind_param("iiiiiiiis", $packing_list_id, $article_id_val, $quantity, $carrier_id, $index, $parent_pli_id, $backpack_id, $backpack_compartment_id, $name_val);
if(!$stmt_insert->execute()) throw new Exception("Fehler beim Einfügen von Artikel " . $article_id . ": " . $stmt_insert->error);
$new_db_id = $conn->insert_id;
$id_map[$pli_id_frontend] = $new_db_id;
@@ -108,21 +128,56 @@ try {
foreach ($child_items as $child) {
$temp_index++;
$stmt_insert->bind_param("iiiiii", $packing_list_id, $child['id'], 1, $carrier_id, $temp_index, $new_db_id);
// Child items are normal items
$stmt_insert->bind_param("iiiiiiiis", $packing_list_id, $child['id'], 1, $carrier_id, $temp_index, $new_db_id, $null_val, $null_val, $null_val);
$null_val = NULL;
if(!$stmt_insert->execute()) throw new Exception("Fehler beim Einfügen von Kind-Artikel " . $child['id']);
}
}
}
}
$stmt_insert->close();
// Post-Fix: Restore names for containers from Backpack tables if they were lost because we didn't send them from frontend.
// Actually, better to just UPDATE them based on IDs after insertion.
// Update Root Names
$conn->query("UPDATE packing_list_items pli JOIN backpacks b ON pli.backpack_id = b.id SET pli.name = CONCAT('Rucksack: ', b.name) WHERE pli.packing_list_id = $packing_list_id AND pli.backpack_id IS NOT NULL");
// Update Compartment Names
$conn->query("UPDATE packing_list_items pli JOIN backpack_compartments bc ON pli.backpack_compartment_id = bc.id SET pli.name = bc.name WHERE pli.packing_list_id = $packing_list_id AND pli.backpack_compartment_id IS NOT NULL");
break;
case 'delete_item':
$item_id = intval($data['item_id']);
$stmt_update_children = $conn->prepare("UPDATE packing_list_items SET parent_packing_list_item_id = NULL WHERE parent_packing_list_item_id = ?");
$stmt_update_children->bind_param("i", $item_id); $stmt_update_children->execute(); $stmt_update_children->close();
$stmt_delete = $conn->prepare("DELETE FROM packing_list_items WHERE id = ? AND packing_list_id = ?");
$stmt_delete->bind_param("ii", $item_id, $packing_list_id); $stmt_delete->execute(); $stmt_delete->close();
// Recursive Delete Logic using CTE if MySQL 8+ or manual recursion
// Manual recursion to be safe on older DBs:
$ids_to_delete = [$item_id];
$i = 0;
while($i < count($ids_to_delete)) {
$current_parent = $ids_to_delete[$i];
$stmt_children = $conn->prepare("SELECT id FROM packing_list_items WHERE parent_packing_list_item_id = ?");
$stmt_children->bind_param("i", $current_parent);
$stmt_children->execute();
$result = $stmt_children->get_result();
while($row = $result->fetch_assoc()) {
$ids_to_delete[] = $row['id'];
}
$stmt_children->close();
$i++;
}
// Delete all gathered IDs
$in_query = implode(',', array_fill(0, count($ids_to_delete), '?'));
$types = str_repeat('i', count($ids_to_delete));
$stmt_delete = $conn->prepare("DELETE FROM packing_list_items WHERE id IN ($in_query) AND packing_list_id = ?");
// Combine ID params and list ID
$delete_params = array_merge($ids_to_delete, [$packing_list_id]);
$delete_types = $types . 'i';
$stmt_delete->bind_param($delete_types, ...$delete_params);
$stmt_delete->execute();
$stmt_delete->close();
break;
case 'update_quantity':
$item_id = intval($data['item_id']);
@@ -169,7 +224,7 @@ 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, a.name, a.weight_grams, a.product_designation, a.consumable, m.name as manufacturer_name FROM packing_list_items pli 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");
$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");
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);

0
articles.php → src/articles.php Normal file → Executable file
View File

19
assets/css/style.css → src/assets/css/style.css Normal file → Executable file
View File

@@ -750,9 +750,22 @@ body::before {
padding: 0.75rem;
}
.toggle-tree-btn {
color: var(--color-secondary);
transition: color 0.2s;
.save-feedback {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: var(--color-primary-dark);
color: #fff;
padding: 10px 20px;
border-radius: 30px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
z-index: 9999;
opacity: 0;
transition: opacity 0.3s ease-in-out;
pointer-events: none;
font-weight: 600;
font-size: 0.9rem;
}
/* --- Articles Table Styles --- */
/* Sticky Header */

73
src/backpack_utils.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
// backpack_utils.php - Shared functions for backpack management
function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
// 1. Get Backpack Info
$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);
$stmt->execute();
$res = $stmt->get_result();
if ($row = $res->fetch_assoc()) {
$root_id = $row['id'];
} else {
// Create Root
$name = "Rucksack: " . $bp['name'];
// Use correct column count and NULLs
$stmt_ins = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, carrier_user_id, name, backpack_id, quantity, article_id, order_index, parent_packing_list_item_id) VALUES (?, ?, ?, ?, 1, NULL, 0, NULL)");
$stmt_ins->bind_param("iisi", $list_id, $user_id, $name, $backpack_id);
$stmt_ins->execute();
$root_id = $stmt_ins->insert_id;
}
// 3. Sync Compartments
$comps = $conn->query("SELECT id, name 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 = ?");
$stmt_c->bind_param("iii", $list_id, $comp['id'], $user_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);
$stmt_ins_c->execute();
}
}
}
// Helper to get backpacks for a specific user context
function get_available_backpacks_for_user($conn, $target_user_id, $household_id) {
// Check connection
if (!$conn || $conn->connect_errno) {
// Reconnect or fail gracefully?
// For now, assume caller must keep it open.
return [];
}
$bps = [];
$sql_bp = "SELECT id, name, user_id FROM backpacks WHERE user_id = ?";
if ($household_id) {
$sql_bp .= " OR household_id = ?";
$stmt_bp = $conn->prepare($sql_bp);
$stmt_bp->bind_param("ii", $target_user_id, $household_id);
} else {
$stmt_bp = $conn->prepare($sql_bp);
$stmt_bp->bind_param("i", $target_user_id);
}
$stmt_bp->execute();
$res_bp = $stmt_bp->get_result();
while ($row = $res_bp->fetch_assoc()) {
$bps[] = $row;
}
return $bps;
}
?>

137
src/backpacks.php Executable file
View File

@@ -0,0 +1,137 @@
<?php
// backpacks.php - Verwaltung der Rucksäcke
$page_title = "Rucksäcke";
require_once 'db_connect.php';
require_once 'header.php';
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
$user_id = $_SESSION['user_id'];
$message = '';
// Delete Action
if (isset($_POST['delete_backpack_id'])) {
$delete_id = intval($_POST['delete_backpack_id']);
// Check ownership
$stmt = $conn->prepare("SELECT id FROM backpacks WHERE id = ? AND user_id = ?");
$stmt->bind_param("ii", $delete_id, $user_id);
$stmt->execute();
if ($stmt->get_result()->num_rows > 0) {
$stmt_del = $conn->prepare("DELETE FROM backpacks WHERE id = ?");
$stmt_del->bind_param("i", $delete_id);
if ($stmt_del->execute()) {
$message = '<div class="alert alert-success">Rucksack gelöscht.</div>';
} else {
$message = '<div class="alert alert-danger">Fehler beim Löschen: ' . $conn->error . '</div>';
}
} else {
$message = '<div class="alert alert-danger">Keine Berechtigung.</div>';
}
}
// Fetch Backpacks (Personal + Household)
// Logic: Show my backpacks AND backpacks from my household (if I'm in one)
$household_id = null;
$stmt_hh = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_hh->bind_param("i", $user_id);
$stmt_hh->execute();
$res_hh = $stmt_hh->get_result();
if ($row = $res_hh->fetch_assoc()) {
$household_id = $row['household_id'];
}
$backpacks = [];
$sql = "SELECT b.*, u.username as owner_name
FROM backpacks b
JOIN users u ON b.user_id = u.id
WHERE b.user_id = ?";
if ($household_id) {
$sql .= " OR (b.household_id = ?)";
}
$sql .= " ORDER BY b.name ASC";
$stmt = $conn->prepare($sql);
if ($household_id) {
$stmt->bind_param("ii", $user_id, $household_id);
} else {
$stmt->bind_param("i", $user_id);
}
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
$backpacks[] = $row;
}
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0"><i class="fas fa-hiking me-2"></i>Rucksäcke</h2>
<a href="edit_backpack.php" class="btn btn-sm btn-outline-light"><i class="fas fa-plus me-2"></i>Neuen Rucksack anlegen</a>
</div>
<div class="card-body">
<?php echo $message; ?>
<?php if (empty($backpacks)): ?>
<div class="alert alert-info text-center">
Du hast noch keine Rucksäcke definiert. <a href="edit_backpack.php" class="alert-link">Lege jetzt deinen ersten Rucksack an!</a>
</div>
<?php else: ?>
<div class="row g-4">
<?php foreach ($backpacks as $bp): ?>
<div class="col-md-6 col-lg-4">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title mb-0"><?php echo htmlspecialchars($bp['name']); ?></h5>
<?php if ($bp['user_id'] == $user_id): ?>
<span class="badge bg-primary">Meiner</span>
<?php else: ?>
<span class="badge bg-secondary">von <?php echo htmlspecialchars($bp['owner_name']); ?></span>
<?php endif; ?>
</div>
<p class="card-text text-muted small mb-2">
<?php echo htmlspecialchars($bp['manufacturer'] . ' ' . $bp['model']); ?>
</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-box-open text-muted me-1"></i> <?php echo $bp['volume_liters']; ?> L</span>
</div>
<!-- Compartments Preview -->
<?php
// Fetch compartment count
$stmt_c = $conn->prepare("SELECT COUNT(*) as cnt FROM backpack_compartments WHERE backpack_id = ?");
$stmt_c->bind_param("i", $bp['id']);
$stmt_c->execute();
$cnt = $stmt_c->get_result()->fetch_assoc()['cnt'];
?>
<p class="small text-muted"><i class="fas fa-layer-group me-1"></i> <?php echo $cnt; ?> Fächer definiert</p>
</div>
<div class="card-footer bg-transparent border-top-0 d-flex justify-content-end gap-2">
<?php if ($bp['user_id'] == $user_id): ?>
<a href="edit_backpack.php?id=<?php echo $bp['id']; ?>" class="btn btn-sm btn-outline-primary"><i class="fas fa-edit"></i> Bearbeiten</a>
<form method="post" onsubmit="return confirm('Rucksack wirklich löschen?');" class="d-inline">
<input type="hidden" name="delete_backpack_id" value="<?php echo $bp['id']; ?>">
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash"></i></button>
</form>
<?php else: ?>
<button class="btn btn-sm btn-outline-secondary" disabled>Nur Eigentümer kann bearbeiten</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php require_once 'footer.php'; ?>

0
categories.php → src/categories.php Normal file → Executable file
View File

0
db_connect.php → src/db_connect.php Normal file → Executable file
View File

0
delete_article.php → src/delete_article.php Normal file → Executable file
View File

View File

View File

0
edit_article.php → src/edit_article.php Normal file → Executable file
View File

249
src/edit_backpack.php Executable file
View File

@@ -0,0 +1,249 @@
<?php
// edit_backpack.php - Erstellen und Bearbeiten von Rucksäcken und Fächern
$page_title = "Rucksack bearbeiten";
require_once 'db_connect.php';
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
$user_id = $_SESSION['user_id'];
$backpack_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$backpack = null;
$compartments = [];
$message = '';
// Check Household
$household_id = null;
$stmt_hh = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_hh->bind_param("i", $user_id);
$stmt_hh->execute();
$res_hh = $stmt_hh->get_result();
if ($row = $res_hh->fetch_assoc()) {
$household_id = $row['household_id'];
}
// Handle Form Submission BEFORE loading header
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$name = trim($_POST['name']);
$manufacturer = trim($_POST['manufacturer']);
$model = trim($_POST['model']);
$weight = intval($_POST['weight_grams']);
$volume = intval($_POST['volume_liters']);
$share_household = isset($_POST['share_household']) ? 1 : 0;
$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=? WHERE id=? AND user_id=?");
$stmt->bind_param("sssiisii", $name, $manufacturer, $model, $weight, $volume, $final_household_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) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("iisssii", $user_id, $final_household_id, $name, $manufacturer, $model, $weight, $volume);
$stmt->execute();
$backpack_id = $stmt->insert_id;
}
// Handle Compartments
if (isset($_POST['compartment_names'])) {
$comp_names = $_POST['compartment_names'];
$comp_ids = $_POST['compartment_ids'] ?? [];
// Get existing IDs to know what to delete
// We need to re-fetch existing just for this logic if not loaded yet, or just assume based on IDs
// Since we haven't loaded existing yet in this flow, let's just operate on IDs.
// First load existing IDs for safety to check ownership indirectly via backpack_id
$existing_ids = [];
$stmt_check = $conn->prepare("SELECT id FROM backpack_compartments WHERE backpack_id = ?");
$stmt_check->bind_param("i", $backpack_id);
$stmt_check->execute();
$res_check = $stmt_check->get_result();
while($row = $res_check->fetch_assoc()) $existing_ids[] = $row['id'];
$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;
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->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->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");
}
}
}
header("Location: backpacks.php");
exit;
}
require_once 'header.php';
// Load existing data (Moved after header inclusion is fine for display logic, but data loading could be before too)
if ($backpack_id > 0) {
$stmt = $conn->prepare("SELECT * FROM backpacks WHERE id = ? AND user_id = ?");
$stmt->bind_param("ii", $backpack_id, $user_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$backpack = $result->fetch_assoc();
// Load Compartments
$stmt_c = $conn->prepare("SELECT * FROM backpack_compartments WHERE backpack_id = ? ORDER BY sort_order ASC");
$stmt_c->bind_param("i", $backpack_id);
$stmt_c->execute();
$res_c = $stmt_c->get_result();
while ($row = $res_c->fetch_assoc()) {
$compartments[] = $row;
}
} else {
$message = '<div class="alert alert-danger">Rucksack nicht gefunden oder keine Berechtigung.</div>';
$backpack_id = 0; // Reset to create mode
}
}
?>
?>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h3 class="h5 mb-0"><?php echo $backpack_id > 0 ? 'Rucksack bearbeiten' : 'Neuen Rucksack anlegen'; ?></h3>
</div>
<div class="card-body">
<?php echo $message; ?>
<form method="post" id="backpackForm">
<div class="row g-3 mb-4">
<div class="col-md-12">
<label class="form-label">Bezeichnung (Spitzname)</label>
<input type="text" name="name" class="form-control" required value="<?php echo htmlspecialchars($backpack['name'] ?? ''); ?>" placeholder="z.B. Mein großer Trekker">
</div>
<div class="col-md-6">
<label class="form-label">Hersteller</label>
<input type="text" name="manufacturer" class="form-control" value="<?php echo htmlspecialchars($backpack['manufacturer'] ?? ''); ?>">
</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); ?>">
</div>
<div class="col-md-6">
<label class="form-label">Volumen (Liter)</label>
<input type="number" name="volume_liters" class="form-control" value="<?php echo htmlspecialchars($backpack['volume_liters'] ?? 0); ?>">
</div>
<div class="col-md-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="share_household" name="share_household" <?php echo (!empty($backpack['household_id']) || $backpack_id == 0) ? 'checked' : ''; ?>>
<label class="form-check-label" for="share_household">Mit Haushalt teilen</label>
</div>
</div>
</div>
<h5 class="border-bottom pb-2 mb-3">Fächeraufteilung</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.
</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>
</div>
<?php else: ?>
<?php foreach ($compartments as $comp): ?>
<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 htmlspecialchars($comp['name']); ?>">
<input type="hidden" name="compartment_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>
<?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>
<div class="d-flex justify-content-between border-top pt-3">
<a href="backpacks.php" class="btn btn-secondary">Abbrechen</a>
<button type="submit" class="btn btn-primary"><i class="fas fa-save me-2"></i>Speichern</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.14.0/Sortable.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('compartments-container');
// Sortable for Compartments
new Sortable(container, {
handle: '.input-group-text',
animation: 150
});
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.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-comp')) {
e.target.closest('.compartment-row').remove();
}
});
});
</script>
<?php require_once 'footer.php'; ?>

232
src/edit_packing_list_details.php Executable file
View File

@@ -0,0 +1,232 @@
<?php
// edit_packing_list_details.php - Bearbeiten von Details und Rucksack-Zuweisung
$page_title = "Packliste Details bearbeiten";
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 'household_actions.php';
require_once 'backpack_utils.php'; // Fix: Include utils
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$message = '';
$packing_list = null;
$can_edit = false;
// --- 1. Permissions & Basic Data ---
if ($packing_list_id > 0) {
$stmt_household_check = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household_check->bind_param("i", $current_user_id);
$stmt_household_check->execute();
$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->bind_param("i", $packing_list_id);
$stmt_list_check->execute();
$result = $stmt_list_check->get_result();
if ($result->num_rows == 1) {
$packing_list = $result->fetch_assoc();
$is_owner = ($packing_list['user_id'] == $current_user_id);
$is_household_list = !empty($packing_list['household_id']);
$is_in_same_household = ($is_household_list && $packing_list['household_id'] == $current_user_household_id);
if ($is_owner || $is_in_same_household) {
$can_edit = true;
} else {
$message = '<div class="alert alert-danger">Keine Berechtigung.</div>';
$packing_list = null;
}
} else {
$message = '<div class="alert alert-warning">Packliste nicht gefunden.</div>';
}
} else {
$message = '<div class="alert alert-danger">Keine ID.</div>';
}
// --- 2. Fetch Data for Dropdowns ---
$available_users = [];
if ($can_edit) {
// Owners: Creator + Household Members (if shared)
if ($packing_list['household_id']) {
$stmt = $conn->prepare("SELECT id, username FROM users WHERE household_id = ?");
$stmt->bind_param("i", $packing_list['household_id']);
} else {
$stmt = $conn->prepare("SELECT id, username FROM users WHERE id = ?");
$stmt->bind_param("i", $packing_list['user_id']);
}
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
$available_users[] = $row;
}
// Current Assignments
$current_assignments = [];
$stmt_ca = $conn->prepare("SELECT user_id, backpack_id FROM packing_list_carriers WHERE packing_list_id = ?");
$stmt_ca->bind_param("i", $packing_list_id);
$stmt_ca->execute();
$res_ca = $stmt_ca->get_result();
while ($row = $res_ca->fetch_assoc()) {
$current_assignments[$row['user_id']] = $row['backpack_id'];
}
}
// --- 3. Handle Form Submission ---
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);
$stmt_update->execute();
$packing_list['name'] = $name;
$packing_list['description'] = $description;
// Handle Backpack Assignments
if (isset($_POST['backpacks'])) {
foreach ($_POST['backpacks'] as $uid => $bid) {
$uid = intval($uid);
$bid = intval($bid);
$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();
} else {
$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)");
}
}
// SYNC LOGIC (Only if new backpack assigned)
if ($bid && $old_backpack_id != $bid) {
sync_backpack_items($conn, $packing_list_id, $uid, $bid);
}
}
}
$message = '<div class="alert alert-success">Änderungen gespeichert!</div>';
// Refresh assignments
$stmt_ca->execute();
$res_ca = $stmt_ca->get_result();
$current_assignments = [];
while ($row = $res_ca->fetch_assoc()) {
$current_assignments[$row['user_id']] = $row['backpack_id'];
}
}
?>
<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>
</div>
<div class="card-body p-4">
<?php echo $message; ?>
<?php if ($packing_list): ?>
<form method="post">
<div class="row">
<div class="col-md-8">
<h5 class="mb-3">Basisdaten</h5>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="name" value="<?php echo htmlspecialchars($packing_list['name']); ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Beschreibung</label>
<textarea class="form-control" name="description" rows="3"><?php echo htmlspecialchars($packing_list['description'] ?: ''); ?></textarea>
</div>
</div>
<div class="col-md-4">
<h5 class="mb-3">Rucksack-Zuweisung</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>
<?php
// Calculate used backpacks (except for the user's own current assignment)
// But we iterate users. So for each user loop, we need to know what OTHERS have assigned.
// Logic: Get all values from $current_assignments.
$all_assigned_backpack_ids = array_values($current_assignments);
foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $packing_list['household_id']);
$my_current_bp_id = $current_assignments[$user['id']] ?? 0;
?>
<div class="mb-3">
<label class="form-label fw-bold"><?php echo htmlspecialchars($user['username']); ?></label>
<select class="form-select form-select-sm" name="backpacks[<?php echo $user['id']; ?>]">
<option value="0">-- Kein Rucksack --</option>
<?php foreach ($user_backpacks as $bp):
// FILTER: If backpack is used by someone else, skip it.
// "Used by someone else" means: ID is in $all_assigned_backpack_ids AND ID != $my_current_bp_id
if (in_array($bp['id'], $all_assigned_backpack_ids) && $bp['id'] != $my_current_bp_id) {
continue;
}
?>
<option value="<?php echo $bp['id']; ?>" <?php echo ($my_current_bp_id == $bp['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($bp['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<hr class="my-4">
<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>
</div>
</form>
<?php endif; ?>
</div>
</div>
<?php require_once 'footer.php'; ?>

0
footer.php → src/footer.php Normal file → Executable file
View File

1
header.php → src/header.php Normal file → Executable file
View File

@@ -53,6 +53,7 @@ if (isset($_SESSION['user_id'])) {
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link" href="index.php"><i class="fas fa-home fa-fw"></i>Start</a></li>
<li class="nav-item"><a class="nav-link" href="articles.php"><i class="fas fa-box fa-fw"></i>Artikel</a></li>
<li class="nav-item"><a class="nav-link" href="backpacks.php"><i class="fas fa-hiking fa-fw"></i>Rucksäcke</a></li>
<li class="nav-item"><a class="nav-link" href="packing_lists.php"><i class="fas fa-clipboard-list fa-fw"></i>Packlisten</a></li>
<li class="nav-item"><a class="nav-link" href="storage_locations.php"><i class="fas fa-archive fa-fw"></i>Lagerorte</a></li>
<li class="nav-item"><a class="nav-link" href="categories.php"><i class="fas fa-tags fa-fw"></i>Kategorien</a></li>

0
help.php → src/help.php Normal file → Executable file
View File

0
household.php → src/household.php Normal file → Executable file
View File

0
household_actions.php → src/household_actions.php Normal file → Executable file
View File

0
index.php → src/index.php Normal file → Executable file
View File

0
keinbild.png → src/keinbild.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

0
login.php → src/login.php Normal file → Executable file
View File

0
logo.png → src/logo.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

0
logout.php → src/logout.php Normal file → Executable file
View File

View File

@@ -69,8 +69,21 @@ $stmt_all_articles->execute();
$all_articles_raw = $stmt_all_articles->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_all_articles->close();
$packed_items_query = $conn->query("SELECT pli.id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id, a.name, a.weight_grams, a.product_designation, a.consumable, m.name as manufacturer_name FROM packing_list_items pli JOIN articles a ON pli.article_id = a.id LEFT JOIN manufacturers m ON a.manufacturer_id = m.id WHERE pli.packing_list_id = {$packing_list_id} ORDER BY pli.order_index ASC");
$packed_items_raw = $packed_items_query ? $packed_items_query->fetch_all(MYSQLI_ASSOC) : [];
$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
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 = ?
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->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)");
@@ -182,6 +195,16 @@ $conn->close();
font-size: 0.8em;
padding-top: 5px;
}
/* Backpack & Compartment Styles */
.backpack-root-item {
background-color: #e8f5e9;
border: 1px solid #2e7d32;
}
.compartment-item {
background-color: #f1f8e9;
border-left: 3px solid #7cb342;
}
</style>
<script>
@@ -289,14 +312,41 @@ $conn->close();
div.className = 'packed-item-container';
div.dataset.itemId = item.id;
div.dataset.articleId = item.article_id;
if(item.backpack_id) div.dataset.backpackId = item.backpack_id;
if(item.backpack_compartment_id) div.dataset.backpackCompartmentId = item.backpack_compartment_id;
let contentClass = 'packed-item-content';
let iconClass = 'fas fa-grip-vertical handle';
let nameDisplay = item.name;
let metaDisplay = item.manufacturer_name ? `<small class="text-muted">(${item.manufacturer_name})</small>` : '';
let controls = `
<input type="number" class="form-control form-control-sm quantity-input text-center mx-2" value="${item.quantity}" min="1">
<button class="btn btn-sm btn-outline-danger remove-item-btn"><i class="fas fa-trash"></i></button>
`;
// Special styling for Containers
if (item.backpack_id) {
contentClass += ' backpack-root-item';
iconClass = 'fas fa-hiking handle me-2';
nameDisplay = `<strong>${item.name}</strong>`;
metaDisplay = '';
// Disable controls for structural items
controls = '';
} else if (item.backpack_compartment_id) {
contentClass += ' compartment-item';
iconClass = 'fas fa-folder-open handle me-2 text-success';
nameDisplay = `<span class="fw-bold text-dark">${item.name}</span>`;
metaDisplay = '';
// Disable controls for structural items
controls = '';
}
div.innerHTML = `
<div class="packed-item-content">
<i class="fas fa-grip-vertical handle"></i>
<span class="item-name">${item.name} <small class="text-muted">(${item.manufacturer_name || '---'})</small></span>
<div class="${contentClass}">
<i class="${iconClass}"></i>
<span class="item-name ms-2">${nameDisplay} ${metaDisplay}</span>
<div class="item-controls">
<input type="number" class="form-control form-control-sm quantity-input text-center mx-2" value="${item.quantity}" min="1">
<button class="btn btn-sm btn-outline-danger remove-item-btn"><i class="fas fa-trash"></i></button>
${controls}
</div>
</div>
<div class="nested-sortable"></div>
@@ -393,12 +443,27 @@ $conn->close();
const pliId = child.dataset.itemId;
const articleId = child.dataset.articleId;
// Check for container data
let bpId = null;
let bpCompId = null;
if (child.querySelector('.backpack-root-item')) {
// Assuming we store this in dataset or infer from something?
// Actually, the sync relies on existing items usually. But for structure,
// we need to persist these IDs if they exist on the element.
// Since createPackedItemDOM doesn't attach them to dataset, let's fix that or rely on existing data logic?
// Better: attach to dataset in createPackedItemDOM
}
// Correction: The frontend re-sends the whole tree. If we don't send bpId, it might be lost if backend just upserts.
// However, backend 'sync_list' usually recreates or updates.
// Let's add data attributes to the DOM first.
payload.list.push({
pli_id: pliId,
article_id: articleId,
carrier_id: carrierId,
parent_pli_id: parentId
parent_pli_id: parentId,
backpack_id: child.dataset.backpackId || null,
backpack_compartment_id: child.dataset.backpackCompartmentId || null
});
const nestedContainer = child.querySelector('.nested-sortable');

0
manufacturers.php → src/manufacturers.php Normal file → Executable file
View File

429
src/packing_list_detail.php Executable file
View File

@@ -0,0 +1,429 @@
<?php
// packing_list_detail.php - Detailansicht einer Packliste
// FINALE, STABILE VERSION: Mit modernem Tree-View, Toggle-Funktion und externem CSS.
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 'header.php';
$current_user_id = $_SESSION['user_id'];
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$packing_list = null;
$total_weight_grams = 0;
$total_consumable_weight = 0;
$weight_by_category = [];
$weight_by_carrier = [];
$weight_by_carrier_non_consumable = [];
if ($packing_list_id <= 0) {
die("Keine Packlisten-ID angegeben.");
}
// Erweiterte Berechtigungsprüfung für Haushalte
$stmt_current_user = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_current_user->bind_param("i", $current_user_id);
$stmt_current_user->execute();
$current_user_household_id = $stmt_current_user->get_result()->fetch_assoc()['household_id'];
$stmt_current_user->close();
$stmt_list_owner = $conn->prepare(
"SELECT pl.*, u.household_id as owner_household_id
FROM packing_lists pl
JOIN users u ON pl.user_id = u.id
WHERE pl.id = ?"
);
$stmt_list_owner->bind_param("i", $packing_list_id);
$stmt_list_owner->execute();
$result = $stmt_list_owner->get_result();
if ($result->num_rows > 0) {
$packing_list = $result->fetch_assoc();
$is_owner = ($packing_list['user_id'] == $current_user_id);
$is_household_list = !empty($packing_list['household_id']);
$is_in_same_household = ($is_household_list && $packing_list['household_id'] == $current_user_household_id);
if (!$is_owner && !$is_in_same_household) {
die("Packliste nicht gefunden oder Zugriff verweigert.");
}
} else {
die("Packliste nicht gefunden oder Zugriff verweigert.");
}
$stmt_list_owner->close();
$page_title = "Packliste: " . htmlspecialchars($packing_list['name']);
// Robust SQL: Fetches names from Backpacks/Compartments if article is missing
$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, 0) as weight_grams,
a.image_url, a.product_designation, a.consumable,
c.name AS category_name,
m.name AS manufacturer_name,
u.username AS carrier_name
FROM packing_list_items AS pli
LEFT JOIN articles AS a ON pli.article_id = a.id
LEFT JOIN backpacks AS bp ON pli.backpack_id = bp.id
LEFT JOIN backpack_compartments AS bpc ON pli.backpack_compartment_id = bpc.id
LEFT JOIN categories AS c ON a.category_id = c.id
LEFT JOIN 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 = ?
ORDER BY pli.order_index ASC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result();
$items_by_id = [];
$items_by_parent = [];
while ($row = $result->fetch_assoc()) {
// Fix Names for Display: Removed redundant prefix logic
// if ($row['backpack_id']) $row['article_name'] = "Rucksack: " . $row['article_name'];
$items_by_id[$row['id']] = $row;
$parent_id = $row['parent_packing_list_item_id'] ?: 0;
if (!isset($items_by_parent[$parent_id])) {
$items_by_parent[$parent_id] = [];
}
$items_by_parent[$parent_id][] = $row;
// Stats
$item_weight = $row['quantity'] * $row['weight_grams'];
$total_weight_grams += $item_weight;
$carrier_name = $row['carrier_name'] ?: 'Sonstiges';
if (!isset($weight_by_carrier[$carrier_name])) $weight_by_carrier[$carrier_name] = 0;
$weight_by_carrier[$carrier_name] += $item_weight;
$cat_name = $row['category_name'] ?: 'Sonstiges';
if (!isset($weight_by_category[$cat_name])) $weight_by_category[$cat_name] = 0;
$weight_by_category[$cat_name] += $item_weight;
if ($row['consumable']) {
$total_consumable_weight += $item_weight;
} else {
if (!isset($weight_by_carrier_non_consumable[$carrier_name])) $weight_by_carrier_non_consumable[$carrier_name] = 0;
$weight_by_carrier_non_consumable[$carrier_name] += $item_weight;
}
}
$stmt->close();
$conn->close();
$total_weight_without_consumables = $total_weight_grams - $total_consumable_weight;
// Helper for recursive counting
function get_recursive_quantity($parent_id, $items_by_parent) {
$count = 0;
if (isset($items_by_parent[$parent_id])) {
foreach ($items_by_parent[$parent_id] as $child) {
$count += $child['quantity'];
$count += get_recursive_quantity($child['id'], $items_by_parent);
}
}
return $count;
}
// Recursive Rendering
function render_item_row($item, $level, $items_by_parent) {
$has_children = isset($items_by_parent[$item['id']]);
$is_backpack = !empty($item['backpack_id']);
$is_compartment = !empty($item['backpack_compartment_id']);
// Visual Styles
$bg_class = "";
$text_class = "";
$icon = "";
if ($is_backpack) {
$bg_class = "table-success"; // Greenish for Backpack
$text_class = "fw-bold text-uppercase";
$icon = '<i class="fas fa-hiking me-2 text-success"></i>';
} elseif ($is_compartment) {
$bg_class = "table-light"; // Light gray for Compartment
$text_class = "fw-bold fst-italic text-muted";
$icon = '<i class="fas fa-folder-open me-2 text-warning"></i>';
} else {
// Standard Item
$img_src = !empty($item['image_url']) ? htmlspecialchars($item['image_url']) : 'keinbild.png';
$icon = '<img src="' . $img_src . '" class="item-image me-2 article-image-trigger" data-preview-url="' . $img_src . '">';
}
$indent_px = $level * 25;
$weight_display = $item['weight_grams'] > 0 ? number_format($item['weight_grams'], 0, ',', '.') . ' g' : '-';
$total_weight_display = $item['weight_grams'] > 0 ? number_format($item['weight_grams'] * $item['quantity'], 0, ',', '.') . ' g' : '-';
echo '<tr class="' . $bg_class . '" data-id="' . $item['id'] . '" data-parent-id="' . ($item['parent_packing_list_item_id'] ?: 0) . '">';
// Name Column with Indentation
echo '<td>';
echo '<div style="padding-left: ' . $indent_px . 'px; display: flex; align-items: center;">';
// Tree Toggle or Spacer
if ($has_children) {
echo '<button class="btn btn-sm btn-link p-0 me-2 text-decoration-none toggle-tree-btn" data-target-id="' . $item['id'] . '"><i class="fas fa-chevron-down"></i></button>';
} else {
echo '<span style="width: 20px; display: inline-block;"></span>';
}
echo $icon;
echo '<span class="' . $text_class . '">' . htmlspecialchars($item['article_name']) . '</span>';
echo '</div>';
echo '</td>';
// Other Columns
echo '<td class="text-center">' . ($item['consumable'] ? '<i class="fas fa-cookie-bite text-warning" title="Verbrauch"></i>' : '') . '</td>';
echo '<td>' . htmlspecialchars($item['manufacturer_name'] ?: '') . '</td>';
echo '<td>' . htmlspecialchars($item['product_designation'] ?: '') . '</td>';
echo '<td>' . htmlspecialchars($item['category_name'] ?: '') . '</td>';
// Quantity / Child Count Badge
echo '<td class="text-center">';
if ($is_backpack) {
// Rucksack: Keine Summe anzeigen (User Wunsch)
echo '';
} elseif ($is_compartment) {
// Fächer: Rekursive Summe aller enthaltenen Artikel
$total_items = get_recursive_quantity($item['id'], $items_by_parent);
if ($total_items > 0) {
// Grün, aber gleiche Form wie Standard (kein rounded-pill)
echo '<span class="badge bg-success text-white border border-success" title="Enthält ' . $total_items . ' Artikel">' . $total_items . '</span>';
}
} else {
// Standard Artikel Menge
echo '<span class="badge bg-white text-dark border">' . $item['quantity'] . 'x</span>';
}
echo '</td>';
echo '<td class="text-end text-muted">' . $weight_display . '</td>';
echo '<td class="text-end fw-bold">' . $total_weight_display . '</td>';
echo '</tr>';
if ($has_children) {
foreach ($items_by_parent[$item['id']] as $child) {
render_item_row($child, $level + 1, $items_by_parent);
}
}
}
function render_print_table_rows($items, $level = 0) {
foreach($items as $item) {
// Simulate structure for print function which might expect different array format?
// Actually, this function is called below with items from $sorted_items_by_carrier which likely won't work
// because we rewrote the main fetching logic to flattened array $items_by_parent.
// We need to adapt the print view section below to use $items_by_parent logic.
}
}
?>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
<div class="screen-view">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h1 class="h4 mb-0"><i class="fas fa-clipboard-list me-2"></i>Packliste: <?php echo htmlspecialchars($packing_list['name']); ?></h1>
<div class="print-hide">
<?php if ($packing_list['user_id'] == $current_user_id || (!empty($packing_list['household_id']) && $packing_list['household_id'] == $current_user_household_id)): ?>
<a href="manage_packing_list_items.php?id=<?php echo $packing_list_id; ?>" class="btn btn-outline-light btn-sm"><i class="fas fa-edit me-2"></i>Bearbeiten</a>
<?php endif; ?>
<a href="packing_lists.php" class="btn btn-outline-light btn-sm ms-2"><i class="fas fa-arrow-left me-2"></i>Zur Übersicht</a>
<button onclick="window.print();" class="btn btn-outline-light btn-sm ms-2"><i class="fas fa-print me-2"></i>Drucken</button>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 mb-4 mb-lg-0">
<div class="card h-100">
<div class="card-header"><h5 class="mb-0"><i class="fas fa-box-open me-2"></i>Gepackte Artikel</h5></div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover table-sm mb-0 align-middle">
<thead class="table-light">
<tr>
<th>Artikel / Struktur</th>
<th class="text-center" style="width: 30px;"><i class="fas fa-cookie-bite"></i></th>
<th>Hersteller</th>
<th>Modell</th>
<th>Kategorie</th>
<th class="text-center">Anz.</th>
<th class="text-end">Gewicht</th>
<th class="text-end">Gesamt</th>
</tr>
</thead>
<tbody>
<?php if (empty($items_by_parent[0])): ?>
<tr><td colspan="8" class="text-center p-4 text-muted">Liste ist leer.</td></tr>
<?php else: ?>
<?php
// Group roots by carrier
$items_by_carrier = [];
foreach ($items_by_parent[0] as $root) {
$c = $root['carrier_name'] ?: 'Sonstiges';
$items_by_carrier[$c][] = $root;
}
foreach ($items_by_carrier as $carrier => $roots): ?>
<tr class="table-secondary border-bottom border-white">
<td colspan="8" class="fw-bold text-uppercase ps-3 py-2"><i class="fas fa-user-circle me-2"></i><?php echo htmlspecialchars($carrier); ?></td>
</tr>
<?php foreach ($roots as $root_item):
render_item_row($root_item, 0, $items_by_parent);
endforeach; ?>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header card-header-stats"><h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>Statistiken</h5></div>
<div class="card-body">
<div class="stats-table-container mb-4">
<h6>Gewicht pro Träger</h6>
<ul class="list-group list-group-flush bg-transparent">
<?php foreach ($weight_by_carrier as $carrier => $weight): ?>
<li class="list-group-item px-1 py-2 bg-transparent">
<strong><?php echo htmlspecialchars($carrier); ?></strong>
<div class="d-flex justify-content-between align-items-center text-muted small mt-1">
Gesamt:<span class="badge bg-success rounded-pill"><?php echo number_format($weight, 0, ',', '.'); ?> g</span>
</div>
<div class="d-flex justify-content-between align-items-center text-muted small">
Basis (o. Verbr.):<span class="badge bg-secondary rounded-pill"><?php echo number_format($weight_by_carrier_non_consumable[$carrier] ?? 0, 0, ',', '.'); ?> g</span>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
<div class="row">
<div class="col-6"><h6 class="text-center small">nach Kategorie</h6><div class="chart-container"><canvas id="categoryWeightChart"></canvas></div></div>
<div class="col-6"><h6 class="text-center small">nach Träger</h6><div class="chart-container"><canvas id="carrierWeightChart"></canvas></div></div>
</div>
<!-- Re-added Category Weight Table -->
<div class="stats-table-container mt-4">
<h6>Gewicht nach Kategorie</h6>
<div class="table-responsive">
<table id="category-weight-table" class="table table-sm table-hover mb-0">
<tbody>
<?php
arsort($weight_by_category);
foreach($weight_by_category as $cat => $w):
?>
<tr>
<td><?php echo htmlspecialchars($cat); ?></td>
<td class="text-end"><?php echo number_format($w, 0, ',', '.'); ?> g</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="print-view">
<!-- Fallback Print View inside Details Page (usually hidden by CSS unless printing page directly) -->
<div class="print-header">
<h1><?php echo htmlspecialchars($packing_list['name']); ?></h1>
</div>
<table class="print-table" style="width:100%; border-collapse:collapse;">
<thead><tr style="border-bottom:2px solid black;"><th>Artikel</th><th>Anzahl</th><th style="text-align:right;">Gewicht</th></tr></thead>
<tbody>
<?php
// Simple flat print fallback
if(isset($items_by_parent[0])) {
foreach($items_by_parent[0] as $root) {
echo '<tr><td>' . htmlspecialchars($root['article_name']) . '</td><td>' . $root['quantity'] . '</td><td style="text-align:right;">' . $root['weight_grams'] . '</td></tr>';
}
}
?>
</tbody>
</table>
</div>
<div id="image-preview-tooltip"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Tooltip logic
const tooltip = document.getElementById('image-preview-tooltip');
if (tooltip) {
document.querySelectorAll('.article-image-trigger').forEach(trigger => {
trigger.addEventListener('mouseover', e => {
const url = e.target.getAttribute('data-preview-url');
if (url && !url.endsWith('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'; });
});
}
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) });
// Chart logic
Chart.register(ChartDataLabels);
const highContrastColors = ['#00429d', '#4771b2', '#73a2c6', '#a4d3d9', '#deeedb', '#ffc993', '#f48f65', '#d45039', '#930000'];
const catCtx = document.getElementById('categoryWeightChart');
if (catCtx) {
new Chart(catCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(array_keys($weight_by_category)); ?>, datasets: [{ data: <?php echo json_encode(array_values($weight_by_category)); ?>, backgroundColor: highContrastColors, borderWidth: 2, hoverOffset: 15 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } } } });
}
const carrierCtx = document.getElementById('carrierWeightChart');
if (carrierCtx) {
new Chart(carrierCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(array_keys($weight_by_carrier)); ?>, datasets: [{ data: <?php echo json_encode(array_values($weight_by_carrier)); ?>, backgroundColor: highContrastColors, borderWidth: 2, hoverOffset: 15 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } } } });
}
// Collapsible Tree Logic
document.querySelectorAll('.toggle-tree-btn').forEach(btn => {
btn.addEventListener('click', function() {
const parentId = this.getAttribute('data-target-id');
const icon = this.querySelector('i');
const isExpanded = icon.classList.contains('fa-chevron-down');
icon.classList.toggle('fa-chevron-down');
icon.classList.toggle('fa-chevron-right');
toggleChildren(parentId, !isExpanded);
});
});
function toggleChildren(parentId, show) {
const directChildren = document.querySelectorAll(`tr[data-parent-id="${parentId}"]`);
directChildren.forEach(row => {
row.style.display = show ? '' : 'none';
const rowId = row.getAttribute('data-id');
if (!show) {
toggleChildren(rowId, false);
} else {
const btn = row.querySelector('.toggle-tree-btn');
if (btn) {
const isExpanded = btn.querySelector('i').classList.contains('fa-chevron-down');
if (isExpanded) {
toggleChildren(rowId, true);
}
}
}
});
}
});
</script>
<?php require_once 'footer.php'; ?>

0
packing_lists.php → src/packing_lists.php Normal file → Executable file
View File

36
print_packing_list.php → src/print_packing_list.php Normal file → Executable file
View File

@@ -51,8 +51,8 @@ if ($packing_list_id > 0) {
}
if ($packing_list) {
// Artikel der Packliste abrufen
$stmt = $conn->prepare("SELECT pli.id AS packing_list_item_id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, a.name, a.weight_grams, c.name AS category_name, a.consumable, a.image_url FROM packing_list_items pli JOIN articles a ON pli.article_id = a.id LEFT JOIN categories c ON a.category_id = c.id WHERE pli.packing_list_id = ? ORDER BY pli.parent_packing_list_item_id ASC, a.name ASC");
// Artikel der Packliste abrufen - LEFT JOIN für Container Support
$stmt = $conn->prepare("SELECT pli.id AS packing_list_item_id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.backpack_id, pli.backpack_compartment_id, COALESCE(a.name, pli.name) AS name, a.weight_grams, c.name AS category_name, a.consumable, a.image_url FROM packing_list_items pli LEFT JOIN articles a ON pli.article_id = a.id LEFT JOIN categories c ON a.category_id = c.id WHERE pli.packing_list_id = ? ORDER BY pli.parent_packing_list_item_id ASC, pli.order_index ASC");
if ($stmt) {
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
@@ -60,7 +60,8 @@ if ($packing_list) {
$raw_packing_list_items = [];
while ($row = $result->fetch_assoc()) {
$raw_packing_list_items[] = $row;
$item_total_weight = $row['weight_grams'] * $row['quantity'];
$item_weight = $row['weight_grams'] ?? 0;
$item_total_weight = $item_weight * $row['quantity'];
$total_weight_grams += $item_total_weight;
$category_name = $row['category_name'] ?: 'Unkategorisiert';
if (!isset($weight_by_category[$category_name])) {
@@ -101,19 +102,32 @@ $total_weight_without_consumables = $total_weight_grams - $total_consumable_weig
function renderPrintablePackingListItemsRecursive($items, $level = 0) {
$html = '<ul class="print-list-level level-' . $level . '">';
foreach ($items as $item) {
$item_total_weight_current = $item['weight_grams'] * $item['quantity'];
// Minimalistische Details für den Druck
$details_string = '(' . number_format($item['weight_grams'], 0, ',', '.') . 'g' . ($item['category_name'] ? ', ' . htmlspecialchars($item['category_name']) : '') . ($item['consumable'] ? ', Verbrauchbar' : '') . ')';
$item_weight = $item['weight_grams'] ?? 0;
$item_total_weight_current = $item_weight * $item['quantity'];
$indent_style = ($level > 0) ? 'padding-left: ' . ($level * 2) . 'em;' : ''; // Deutlichere Einrückung im Druck
// Container Styling
$is_container = !empty($item['backpack_id']) || !empty($item['backpack_compartment_id']);
$container_style = "";
if (!empty($item['backpack_id'])) {
$container_style = "font-weight: bold; font-size: 1.1em; margin-top: 10px; border-bottom: 2px solid #ccc;";
} elseif (!empty($item['backpack_compartment_id'])) {
$container_style = "font-weight: bold; margin-top: 5px; color: #555;";
}
$details_string = '';
if (!$is_container) {
$details_string = '(' . number_format($item_weight, 0, ',', '.') . 'g' . ($item['category_name'] ? ', ' . htmlspecialchars($item['category_name']) : '') . ($item['consumable'] ? ', Verbrauchbar' : '') . ')';
}
$indent_style = ($level > 0) ? 'padding-left: ' . ($level * 1.5) . 'em;' : '';
$html .= '
<li class="print-list-item" style="' . $indent_style . '">
<span class="print-checkbox"></span>
<span class="print-quantity">' . htmlspecialchars($item['quantity']) . 'x</span>
<li class="print-list-item" style="' . $indent_style . ' ' . $container_style . '">
<span class="print-checkbox" style="' . ($is_container ? 'visibility:hidden;' : '') . '"></span>
<span class="print-quantity">' . ($is_container ? '' : htmlspecialchars($item['quantity']) . 'x') . '</span>
<span class="print-name">' . htmlspecialchars($item['name']) . '</span>
<span class="print-details">' . $details_string . '</span>
<span class="print-total-weight">' . number_format($item_total_weight_current, 0, ',', '.') . 'g</span>
<span class="print-total-weight">' . ($is_container ? '' : number_format($item_total_weight_current, 0, ',', '.') . 'g') . '</span>
</li>';
if (!empty($item['children'])) {
$html .= renderPrintablePackingListItemsRecursive($item['children'], $level + 1);

0
public_list.php → src/public_list.php Normal file → Executable file
View File

0
register.php → src/register.php Normal file → Executable file
View File

0
rucksack_icon.png → src/rucksack_icon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

0
share_packing_list.php → src/share_packing_list.php Normal file → Executable file
View File

0
storage_locations.php → src/storage_locations.php Normal file → Executable file
View File

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 580 KiB

After

Width:  |  Height:  |  Size: 580 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 668 KiB

After

Width:  |  Height:  |  Size: 668 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

Before

Width:  |  Height:  |  Size: 862 KiB

After

Width:  |  Height:  |  Size: 862 KiB

View File

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 422 KiB

View File

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 422 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 196 KiB

View File

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 218 KiB

View File

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 211 KiB

View File

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 203 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Some files were not shown because too many files have changed in this diff Show More