Rucksack-Feature finalisiert: Management, Zuweisung und Anzeige implementiert
68
README.md
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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'; ?>
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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
192
src/add_packing_list.php
Executable 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'; ?>
|
||||
71
api_packing_list_handler.php → src/api_packing_list_handler.php
Normal file → Executable 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
19
assets/css/style.css → src/assets/css/style.css
Normal file → Executable 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
@@ -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
@@ -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
0
db_connect.php → src/db_connect.php
Normal file → Executable file
0
delete_article.php → src/delete_article.php
Normal file → Executable file
0
delete_packing_list.php → src/delete_packing_list.php
Normal file → Executable file
0
duplicate_packing_list.php → src/duplicate_packing_list.php
Normal file → Executable file
0
edit_article.php → src/edit_article.php
Normal file → Executable file
249
src/edit_backpack.php
Executable 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
@@ -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
1
header.php → src/header.php
Normal file → Executable 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
0
household.php → src/household.php
Normal file → Executable file
0
household_actions.php → src/household_actions.php
Normal file → Executable file
0
index.php → src/index.php
Normal file → Executable file
0
keinbild.png → src/keinbild.png
Normal file → Executable file
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
0
login.php → src/login.php
Normal file → Executable file
0
logo.png → src/logo.png
Normal file → Executable 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
81
manage_packing_list_items.php → src/manage_packing_list_items.php
Normal file → Executable 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
429
src/packing_list_detail.php
Executable 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
36
print_packing_list.php → src/print_packing_list.php
Normal file → Executable 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
0
register.php → src/register.php
Normal file → Executable file
0
rucksack_icon.png → src/rucksack_icon.png
Normal file → Executable 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
0
storage_locations.php → src/storage_locations.php
Normal file → Executable file
0
uploads/6852fc1190bc2.png → src/uploads/6852fc1190bc2.png
Normal file → Executable file
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
0
uploads/685306028b828.png → src/uploads/685306028b828.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/685306a5be4af.png → src/uploads/685306a5be4af.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/68530b5956db3.png → src/uploads/68530b5956db3.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/685314e648206.png → src/uploads/685314e648206.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/6853157b136ce.png → src/uploads/6853157b136ce.png
Normal file → Executable file
|
Before Width: | Height: | Size: 580 KiB After Width: | Height: | Size: 580 KiB |
0
uploads/images/img_68519d40cefc91.06373491.png → src/uploads/images/img_68519d40cefc91.06373491.png
Normal file → Executable file
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 236 KiB |
0
uploads/images/img_68528c880b1401.14908112.jpg → src/uploads/images/img_68528c880b1401.14908112.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
0
uploads/images/img_6852aa2e9a9ac8.18122970.png → src/uploads/images/img_6852aa2e9a9ac8.18122970.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_6852aae52a8590.99789219.png → src/uploads/images/img_6852aae52a8590.99789219.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_6852d1707d0af4.43646571.png → src/uploads/images/img_6852d1707d0af4.43646571.png
Normal file → Executable file
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
0
uploads/images/img_68530888b01da3.01042792.png → src/uploads/images/img_68530888b01da3.01042792.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_685308945b37e1.08844156.png → src/uploads/images/img_685308945b37e1.08844156.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_6853093a3b0fb7.10685249.png → src/uploads/images/img_6853093a3b0fb7.10685249.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68530974a15736.57910512.png → src/uploads/images/img_68530974a15736.57910512.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_685309b775df63.28364476.png → src/uploads/images/img_685309b775df63.28364476.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68530a6f2c08f5.96614673.png → src/uploads/images/img_68530a6f2c08f5.96614673.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68530aff6faf10.53187686.png → src/uploads/images/img_68530aff6faf10.53187686.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68530ba765f0c1.87888941.png → src/uploads/images/img_68530ba765f0c1.87888941.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68530bdb75d1a9.18204952.png → src/uploads/images/img_68530bdb75d1a9.18204952.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68530c8fc7ce78.25162975.png → src/uploads/images/img_68530c8fc7ce78.25162975.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68530d38ce6cd7.76753737.png → src/uploads/images/img_68530d38ce6cd7.76753737.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68530e3263a8f3.11354541.png → src/uploads/images/img_68530e3263a8f3.11354541.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68530fbf34fd66.94667834.png → src/uploads/images/img_68530fbf34fd66.94667834.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_685310c0348007.20030955.png → src/uploads/images/img_685310c0348007.20030955.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68531110a66323.47909040.png → src/uploads/images/img_68531110a66323.47909040.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_685311fd9dc421.95301293.png → src/uploads/images/img_685311fd9dc421.95301293.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_685312afc6c000.67281690.png → src/uploads/images/img_685312afc6c000.67281690.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_685313c68df985.82769092.png → src/uploads/images/img_685313c68df985.82769092.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_685314c336de58.35409547.png → src/uploads/images/img_685314c336de58.35409547.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68531517362e81.34960172.png → src/uploads/images/img_68531517362e81.34960172.png
Normal file → Executable file
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 236 KiB |
0
uploads/images/img_685316a6098399.74821974.png → src/uploads/images/img_685316a6098399.74821974.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68531a8befbae4.75954207.png → src/uploads/images/img_68531a8befbae4.75954207.png
Normal file → Executable file
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 768 KiB |
0
uploads/images/img_68555c338e1915.37648028.png → src/uploads/images/img_68555c338e1915.37648028.png
Normal file → Executable file
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 236 KiB |
0
uploads/images/img_68555e40030f77.87225795.png → src/uploads/images/img_68555e40030f77.87225795.png
Normal file → Executable file
|
Before Width: | Height: | Size: 668 KiB After Width: | Height: | Size: 668 KiB |
0
uploads/images/img_68692cad8256a4.61815158.png → src/uploads/images/img_68692cad8256a4.61815158.png
Normal file → Executable file
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
0
uploads/images/img_686eade3358e94.97676456.jpg → src/uploads/images/img_686eade3358e94.97676456.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
0
uploads/images/img_686eb198525ec7.50536364.jpg → src/uploads/images/img_686eb198525ec7.50536364.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 862 KiB After Width: | Height: | Size: 862 KiB |
0
uploads/images/img_686eb1ef18f034.45725063.png → src/uploads/images/img_686eb1ef18f034.45725063.png
Normal file → Executable file
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 422 KiB |
0
uploads/images/img_686eb212d64645.26126428.png → src/uploads/images/img_686eb212d64645.26126428.png
Normal file → Executable file
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 422 KiB |
0
uploads/images/img_686eb271e0c6a9.39415484.jpg → src/uploads/images/img_686eb271e0c6a9.39415484.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
0
uploads/images/img_686eb3086df945.36369833.png → src/uploads/images/img_686eb3086df945.36369833.png
Normal file → Executable file
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 196 KiB |
0
uploads/images/img_686eb33807fd63.00035931.png → src/uploads/images/img_686eb33807fd63.00035931.png
Normal file → Executable file
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
0
uploads/images/img_686eb5bc838ae5.80233818.png → src/uploads/images/img_686eb5bc838ae5.80233818.png
Normal file → Executable file
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 211 KiB |
0
uploads/images/img_686ebd1e6af955.32076521.png → src/uploads/images/img_686ebd1e6af955.32076521.png
Normal file → Executable file
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
0
uploads/images/img_686ebdb32de4d3.21230679.jpg → src/uploads/images/img_686ebdb32de4d3.21230679.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
0
uploads/images/img_686ebe5846ea42.68414437.jpg → src/uploads/images/img_686ebe5846ea42.68414437.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
0
uploads/images/img_686ebeb3473cf8.68015324.jpg → src/uploads/images/img_686ebeb3473cf8.68015324.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
0
uploads/images/img_686ebeec251778.89470573.jpg → src/uploads/images/img_686ebeec251778.89470573.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
0
uploads/images/img_686ebfa1bee290.81344086.png → src/uploads/images/img_686ebfa1bee290.81344086.png
Normal file → Executable file
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 203 KiB |
0
uploads/images/img_686ec0042d3ad8.71473135.jpg → src/uploads/images/img_686ec0042d3ad8.71473135.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
0
uploads/images/img_686ec04d316485.12649368.png → src/uploads/images/img_686ec04d316485.12649368.png
Normal file → Executable file
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |
0
uploads/images/img_686ec070bc29e1.89910936.jpg → src/uploads/images/img_686ec070bc29e1.89910936.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
0
uploads/images/img_686f30ac792db4.81124214.jpg → src/uploads/images/img_686f30ac792db4.81124214.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
0
uploads/images/img_686f30d181b9d3.27666927.jpg → src/uploads/images/img_686f30d181b9d3.27666927.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
0
uploads/images/img_686f30fc1384a1.47381841.jpg → src/uploads/images/img_686f30fc1384a1.47381841.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
0
uploads/images/img_686f31607f1ef4.88047468.jpg → src/uploads/images/img_686f31607f1ef4.88047468.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
0
uploads/images/img_686f317edd6d15.06826823.jpg → src/uploads/images/img_686f317edd6d15.06826823.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
0
uploads/images/img_686f31c24a1eb8.68836662.png → src/uploads/images/img_686f31c24a1eb8.68836662.png
Normal file → Executable file
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 118 KiB |