Final Release: Clean Structure, README added

This commit is contained in:
Gemini Bot
2025-12-03 23:23:12 +00:00
commit 8071489ca2
216 changed files with 6623 additions and 0 deletions

61
README.md Normal file
View File

@@ -0,0 +1,61 @@
# Trekking Packliste
Eine moderne, webbasierte Anwendung zur Verwaltung von Packlisten für Wanderungen, Trekking-Touren und Reisen. Ermöglicht das Erstellen von Artikeln, das Organisieren in hierarchischen Packlisten und die Zusammenarbeit in Haushalten.
## Funktionen
* **Artikelverwaltung:** Erfassen von Ausrüstungsgegenständen mit Gewicht, Kategorie, Hersteller und Bildern.
* **Packlisten:** Zusammenstellen von Artikeln zu Listen. Unterstützt Drag & Drop mit Verschachtelung (z.B. Rucksack -> Packsack -> Socken).
* **Haushalte:** Gemeinsame Nutzung von Artikeln und Listen mit anderen Benutzern.
* **Gewichtskalkulation:** Automatische Berechnung des Gesamtgewichts und Aufschlüsselung nach Kategorien und Trägern.
* **Druckansicht:** Optimierte Ansicht für den Ausdruck der Packliste.
* **Modernes UI:** Responsives Design mit Glassmorphism-Effekten und Dark-Mode-Ansätzen (Grünes Theme).
## Voraussetzungen
* Webserver (Apache, Nginx)
* PHP 8.0 oder höher
* MariaDB / MySQL Datenbank
* PHP-Erweiterungen: `mysqli`, `gd` (für Bildbearbeitung)
## Installation
1. **Dateien kopieren:** Lade den Inhalt dieses Repositories in das Web-Verzeichnis deines Servers hoch.
2. **Datenbank:**
* Erstelle eine neue Datenbank (z.B. `packliste`).
* Importiere die Struktur (Tabellen) in die Datenbank. (Ein SQL-Schema sollte separat bereitgestellt werden oder existiert bereits).
3. **Konfiguration:**
* Erstelle eine Datei `config.ini` **außerhalb** des öffentlichen Web-Verzeichnisses (aus Sicherheitsgründen empfohlen) oder schütze sie per `.htaccess`.
* Standardmäßig sucht die Anwendung die `config.ini` ein Verzeichnis **über** dem Webroot (`../config.ini`).
### Aufbau der config.ini
Die Datei muss folgende Struktur haben:
```ini
servername = localhost
username = dein_db_benutzer
password = dein_db_passwort
dbname = deine_datenbank_name
```
### Upload-Ordner
Stelle sicher, dass der Ordner `uploads/` und `uploads/images/` existiert und vom Webserver beschreibbar ist (`chmod 755` oder `777` je nach Server-Konfiguration).
## Nutzung
1. Registriere dich auf der Startseite.
2. Lege im Profil deine Einstellungen fest.
3. Erstelle Artikel unter "Artikel".
4. Erstelle eine Packliste und füge Artikel per Drag & Drop hinzu.
## Technologie
* **Backend:** Natives PHP 8.2
* **Datenbank:** MariaDB
* **Frontend:** HTML5, CSS3 (Custom Glassmorphism Theme), Bootstrap 5
* **Bibliotheken:**
* Sortable.js (Drag & Drop)
* Chart.js (Statistiken)
* FontAwesome (Icons)

353
add_article.php Normal file
View File

@@ -0,0 +1,353 @@
<?php
// add_article.php - Formular zum Hinzufügen eines neuen Artikels
// FINALE, VOLLSTÄNDIGE VERSION mit allen Funktionen (Komponenten, Bestand, etc.)
// KORREKTUR: SSL-Verifizierung und @-Fehlerunterdrückung korrigiert.
$page_title = "Neuen Artikel hinzufügen";
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 'header.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
$name = ''; $weight_grams = ''; $category_id = null; $consumable = 0; $image_url = '';
$product_url = ''; $manufacturer_id = null; $product_designation = '';
$storage_location_id = null; $quantity_owned = 1; $parent_article_id = null;
// Lade Haushalts-ID und Mitglieder-IDs
$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();
$household_member_ids = [$current_user_id];
if ($household_id_for_user) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $household_id_for_user);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) {
if (!in_array($row['id'], $household_member_ids)) { $household_member_ids[] = $row['id']; }
}
$stmt_members->close();
}
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
// Lade Kategorien, Hersteller, Lagerorte und potenzielle Eltern-Artikel
$stmt_cat_load = $conn->prepare("SELECT id, name FROM categories WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_cat_load->bind_param($types, ...$household_member_ids);
$stmt_cat_load->execute();
$categories = $stmt_cat_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_cat_load->close();
$stmt_man_load = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_man_load->bind_param($types, ...$household_member_ids);
$stmt_man_load->execute();
$manufacturers = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_man_load->close();
$stmt_loc_load = $conn->prepare("SELECT id, name, parent_id FROM storage_locations WHERE user_id IN ($placeholders) ORDER BY parent_id, name");
$stmt_loc_load->bind_param($types, ...$household_member_ids);
$stmt_loc_load->execute();
$all_locations = $stmt_loc_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_loc_load->close();
$all_parent_params = array_merge($household_member_ids, [$household_id_for_user]);
$all_parent_types = $types . 'i';
$stmt_parent_articles = $conn->prepare("SELECT a.id, a.name, m.name as manufacturer_name, a.product_designation FROM articles a LEFT JOIN manufacturers m ON a.manufacturer_id = m.id WHERE (a.user_id IN ($placeholders) OR a.household_id = ?) AND a.parent_article_id IS NULL ORDER BY a.name ASC");
$stmt_parent_articles->bind_param($all_parent_types, ...$all_parent_params);
$stmt_parent_articles->execute();
$parent_articles = $stmt_parent_articles->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_parent_articles->close();
$storage_locations_structured = [];
foreach ($all_locations as $loc) { if ($loc['parent_id'] === NULL) { if (!isset($storage_locations_structured[$loc['id']])) { $storage_locations_structured[$loc['id']] = ['name' => $loc['name'], 'children' => []]; } } }
foreach ($all_locations as $loc) { if ($loc['parent_id'] !== NULL && isset($storage_locations_structured[$loc['parent_id']])) { $storage_locations_structured[$loc['parent_id']]['children'][] = $loc; } }
$upload_dir = 'uploads/images/';
if (!is_dir($upload_dir)) { @mkdir($upload_dir, 0777, true); }
function save_image_from_url($url, $upload_dir) {
// KORREKTUR: Unsichere SSL-Optionen entfernt, @-Operator entfernt
$context_options = ["http" => ["header" => "User-Agent: Mozilla/5.0\r\n", "timeout" => 10]];
$context = stream_context_create($context_options);
// Fehlerbehandlung für file_get_contents
$image_data = file_get_contents($url, false, $context);
if ($image_data === false) {
return [false, "Bild konnte von der URL nicht heruntergeladen werden. Überprüfen Sie den Link und die Erreichbarkeit."];
}
// Fehlerbehandlung für getimagesizefromstring
$image_info = getimagesizefromstring($image_data);
if ($image_info === false) {
return [false, "Die angegebene URL führt nicht zu einem gültigen Bild."];
}
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($image_info['mime'], $allowed_mime_types)) {
return [false, "Nicht unterstützter Bildtyp: " . htmlspecialchars($image_info['mime'])];
}
$extension = image_type_to_extension($image_info[2], false);
$unique_file_name = uniqid('img_url_', true) . '.' . $extension;
$destination = $upload_dir . $unique_file_name;
if (file_put_contents($destination, $image_data)) {
return [true, $destination];
}
return [false, "Bild konnte nicht auf dem Server gespeichert werden."];
}
function save_image_from_base64($base64_string, $upload_dir) {
if (preg_match('/^data:image\/(\w+);base64,/', $base64_string, $type)) {
$data = substr($base64_string, strpos($base64_string, ',') + 1);
$type = strtolower($type[1]);
if (!in_array($type, ['jpg', 'jpeg', 'png', 'gif'])) { return [false, "Nicht unterstützter Bildtyp aus der Zwischenablage."]; }
$data = base64_decode($data);
if ($data === false) { return [false, "Base64-Dekodierung fehlgeschlagen."]; }
} else { return [false, "Ungültiger Base64-String."]; }
$unique_file_name = uniqid('img_paste_', true) . '.' . $type;
$destination = $upload_dir . $unique_file_name;
if (file_put_contents($destination, $data)) { return [true, $destination]; }
return [false, "Bild aus der Zwischenablage konnte nicht gespeichert werden."];
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$name = trim($_POST['name']);
$weight_grams = intval($_POST['weight_grams']);
$consumable = isset($_POST['consumable']) ? 1 : 0;
$quantity_owned = ($consumable == 1) ? 1 : intval($_POST['quantity_owned']);
$is_household_item = isset($_POST['is_household_item']) && $household_id_for_user ? $household_id_for_user : NULL;
$storage_location_id = !empty($_POST['storage_location_id']) ? intval($_POST['storage_location_id']) : NULL;
$parent_article_id = !empty($_POST['parent_article_id']) ? intval($_POST['parent_article_id']) : NULL;
$image_url_from_input = trim($_POST['image_url']);
$product_url = trim($_POST['product_url']);
$product_designation = trim($_POST['product_designation']);
$pasted_image_data = $_POST['pasted_image_data'] ?? '';
if (isset($_POST['manufacturer_id']) && $_POST['manufacturer_id'] === 'new') {
$new_manufacturer_name = trim($_POST['new_manufacturer_name']);
if (!empty($new_manufacturer_name)) {
$stmt_man = $conn->prepare("INSERT INTO manufacturers (name, user_id) VALUES (?, ?)");
$stmt_man->bind_param("si", $new_manufacturer_name, $current_user_id);
if ($stmt_man->execute()) { $manufacturer_id = $conn->insert_id; }
$stmt_man->close();
}
} else { $manufacturer_id = !empty($_POST['manufacturer_id']) ? intval($_POST['manufacturer_id']) : NULL; }
if (isset($_POST['category_id']) && $_POST['category_id'] === 'new') {
$new_category_name = trim($_POST['new_category_name']);
if (!empty($new_category_name)) {
$stmt_cat = $conn->prepare("INSERT INTO categories (name, user_id) VALUES (?, ?)");
$stmt_cat->bind_param("si", $new_category_name, $current_user_id);
if($stmt_cat->execute()){ $category_id = $conn->insert_id; }
$stmt_cat->close();
}
} else { $category_id = !empty($_POST['category_id']) ? intval($_POST['category_id']) : NULL; }
$image_url_for_db = NULL;
$image_error = '';
if (!empty($pasted_image_data)) {
list($success, $result) = save_image_from_base64($pasted_image_data, $upload_dir);
if ($success) { $image_url_for_db = $result; } else { $image_error = $result; }
}
elseif (isset($_FILES['image_file']) && $_FILES['image_file']['error'] == UPLOAD_ERR_OK) {
$unique_file_name = uniqid('img_', true) . '.' . strtolower(pathinfo($_FILES['image_file']['name'], PATHINFO_EXTENSION));
$destination = $upload_dir . $unique_file_name;
if (move_uploaded_file($_FILES['image_file']['tmp_name'], $destination)) {
$image_url_for_db = $destination;
} else { $image_error = "Fehler beim Verschieben der hochgeladenen Datei."; }
}
elseif (!empty($image_url_from_input)) {
list($success, $result) = save_image_from_url($image_url_from_input, $upload_dir);
if ($success) { $image_url_for_db = $result; } else { $image_error = $result; }
}
if (!empty($image_error)) {
$message .= '<div class="alert alert-warning" role="alert">Bild-Fehler: ' . htmlspecialchars($image_error) . ' Der Artikel wurde ohne Bild gespeichert.</div>';
}
$product_url_for_db = !empty($product_url) ? $product_url : NULL;
$product_designation_for_db = !empty($product_designation) ? $product_designation : NULL;
if (empty($name) || $weight_grams < 0) {
$message = '<div class="alert alert-danger" role="alert">Bitte geben Sie einen Namen und ein gültiges, nicht-negatives Gewicht an.</div>';
} else {
$stmt_insert_article = $conn->prepare("INSERT INTO articles (user_id, household_id, name, weight_grams, quantity_owned, category_id, consumable, image_url, product_url, manufacturer_id, product_designation, storage_location_id, parent_article_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
if ($stmt_insert_article) {
$stmt_insert_article->bind_param("iisiiisssisii", $current_user_id, $is_household_item, $name, $weight_grams, $quantity_owned, $category_id, $consumable, $image_url_for_db, $product_url_for_db, $manufacturer_id, $product_designation_for_db, $storage_location_id, $parent_article_id);
if ($stmt_insert_article->execute()) {
if ($household_id_for_user) {
$log_message = htmlspecialchars($_SESSION['username']) . " hat den Artikel '" . htmlspecialchars($name) . "' hinzugefügt.";
log_household_action($conn, $household_id_for_user, $current_user_id, $log_message);
}
$message .= '<div class="alert alert-success" role="alert">Artikel "' . htmlspecialchars($name) . '" erfolgreich hinzugefügt! <a href="articles.php" class="alert-link">Zurück zur Übersicht</a>.</div>';
$name = $weight_grams = $image_url = $product_url = $product_designation = '';
$consumable = 0; $quantity_owned = 1; $category_id = $manufacturer_id = $storage_location_id = $parent_article_id = null;
} else {
$message = '<div class="alert alert-danger" role="alert">Fehler beim Hinzufügen des Artikels: ' . $stmt_insert_article->error . '</div>';
}
$stmt_insert_article->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>Neuen Artikel erstellen</h2>
<a href="articles.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="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post" enctype="multipart/form-data">
<input type="hidden" name="pasted_image_data" id="pasted_image_data">
<div class="row g-4">
<div class="col-lg-7 d-flex flex-column">
<div class="card section-card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>Stammdaten</h5></div>
<div class="card-body">
<div class="mb-3"><label for="name" class="form-label">Artikelname*</label><input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($name); ?>" required></div>
<div class="row">
<div class="col-md-6 mb-3"><label for="manufacturer_id" class="form-label">Hersteller</label><select class="form-select" id="manufacturer_id" name="manufacturer_id"><option value="">-- Kein Hersteller --</option><option value="new">-- Neuen Hersteller hinzufügen --</option><?php foreach ($manufacturers as $man): ?><option value="<?php echo htmlspecialchars($man['id']); ?>"><?php echo htmlspecialchars($man['name']); ?></option><?php endforeach; ?></select><div id="new_manufacturer_container" class="mt-2" style="display: none;"><input type="text" class="form-control" name="new_manufacturer_name" placeholder="Name des neuen Herstellers"></div></div>
<div class="col-md-6 mb-3"><label for="product_designation" class="form-label">Produktbezeichnung / Modell</label><input type="text" class="form-control" id="product_designation" name="product_designation" value="<?php echo htmlspecialchars($product_designation); ?>" placeholder="z.B. Forclaz MT500"></div>
</div>
<div class="mb-3">
<label for="parent_article_id" class="form-label">Ist Komponente von (optional)</label>
<select class="form-select" id="parent_article_id" name="parent_article_id">
<option value="">-- Kein Hauptartikel --</option>
<?php foreach($parent_articles as $parent):
$parent_details = $parent['manufacturer_name'] ? htmlspecialchars($parent['manufacturer_name']) . ' - ' : '';
$parent_details .= htmlspecialchars($parent['product_designation'] ?? '');
?>
<option value="<?php echo $parent['id']; ?>"><?php echo htmlspecialchars($parent['name'] . ' (' . $parent_details . ')'); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="card section-card">
<div class="card-header"><h5 class="mb-0"><i class="fas fa-cogs me-2"></i>Eigenschaften & Ort</h5></div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3"><label for="weight_grams" class="form-label">Gewicht*</label><div class="input-group"><span class="input-group-text"><i class="fas fa-weight-hanging"></i></span><input type="number" class="form-control" id="weight_grams" name="weight_grams" value="<?php echo htmlspecialchars($weight_grams); ?>" required min="0"><span class="input-group-text">g</span></div></div>
<div class="col-md-6 mb-3"><label for="quantity_owned" class="form-label">Anzahl im Besitz</label><input type="number" class="form-control" id="quantity_owned" name="quantity_owned" value="<?php echo htmlspecialchars($quantity_owned); ?>" min="1"></div>
<div class="col-12 mb-3"><label for="category_id" class="form-label">Kategorie</label><select class="form-select" id="category_id" name="category_id"><option value="">-- Keine --</option><option value="new">-- Neue hinzufügen --</option><?php foreach ($categories as $cat): ?><option value="<?php echo htmlspecialchars($cat['id']); ?>"><?php echo htmlspecialchars($cat['name']); ?></option><?php endforeach; ?></select><div id="new_category_container" class="mt-2" style="display: none;"><input type="text" class="form-control" name="new_category_name" placeholder="Name der neuen Kategorie"></div></div>
<div class="col-12 mb-3"><label for="storage_location_id" class="form-label">Lagerort</label><select class="form-select" id="storage_location_id" name="storage_location_id"><option value="">-- Kein fester Ort --</option><?php foreach ($storage_locations_structured as $loc1_id => $loc1_data): ?><optgroup label="<?php echo htmlspecialchars($loc1_data['name']); ?>"><?php foreach ($loc1_data['children'] as $loc2): ?><option value="<?php echo $loc2['id']; ?>"><?php echo htmlspecialchars($loc2['name']); ?></option><?php endforeach; ?></optgroup><?php endforeach; ?></select></div>
</div>
<hr class="my-3">
<div class="form-check form-switch mb-2"><input class="form-check-input" type="checkbox" role="switch" id="consumable" name="consumable" value="1"><label class="form-check-label" for="consumable">Verbrauchsartikel (unbegrenzte Anzahl)</label></div>
<?php if ($household_id_for_user): ?>
<div class="form-check form-switch"><input class="form-check-input" type="checkbox" role="switch" id="is_household_item" name="is_household_item" value="1"><label class="form-check-label" for="is_household_item">Für den gesamten Haushalt freigeben</label></div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card section-card h-100">
<div class="card-header"><h5 class="mb-0"><i class="fas fa-image me-2"></i>Bild & Produktseite</h5></div>
<div class="card-body d-flex flex-column">
<div class="mb-3"><label for="product_url" class="form-label">Produktseite (URL)</label><input type="url" class="form-control" id="product_url" name="product_url" value="<?php echo htmlspecialchars($product_url); ?>" placeholder="https://..."></div>
<div class="mb-3"><label for="image_file" class="form-label">1. Bild hochladen</label><input class="form-control" type="file" id="image_file" name="image_file" accept="image/*"></div>
<div class="mb-3"><label for="image_url" class="form-label">2. ODER Bild-URL angeben</label><input type="url" class="form-control" id="image_url" name="image_url" placeholder="https://..."></div>
<div class="mb-3"><label class="form-label">3. ODER Bild einfügen (Strg+V)</label><div id="pasteArea" class="paste-area"><i class="fas fa-paste"></i> Hier klicken & einfügen</div></div>
<div class="text-center mt-auto"><label class="form-label d-block">Vorschau</label><img id="imagePreview" src="keinbild.png" class="mb-2"></div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-start align-items-center mt-4 border-top pt-4">
<button type="submit" class="btn btn-primary btn-lg me-3"><i class="fas fa-plus-circle me-2"></i>Artikel erstellen</button>
<a href="articles.php" class="btn btn-secondary">Abbrechen</a>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
function setupAddNewOption(selectId, containerId) {
const select = document.getElementById(selectId);
const container = document.getElementById(containerId);
if (!select || !container) return;
const input = container.querySelector('input');
function toggle() {
const isNew = select.value === 'new';
container.style.display = isNew ? 'block' : 'none';
if (input) input.required = isNew;
if (!isNew && input) { input.value = ''; }
}
select.addEventListener('change', toggle);
toggle();
}
setupAddNewOption('manufacturer_id', 'new_manufacturer_container');
setupAddNewOption('category_id', 'new_category_container');
const imageFileInput = document.getElementById('image_file');
const imageUrlInput = document.getElementById('image_url');
const imagePreview = document.getElementById('imagePreview');
const pasteArea = document.getElementById('pasteArea');
const pastedImageDataInput = document.getElementById('pasted_image_data');
function clearOtherImageInputs(source) {
if (source !== 'file') imageFileInput.value = '';
if (source !== 'url') imageUrlInput.value = '';
if (source !== 'paste') pastedImageDataInput.value = '';
}
if(imageFileInput) {
imageFileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) { imagePreview.src = e.target.result; }
reader.readAsDataURL(this.files[0]);
clearOtherImageInputs('file');
}
});
}
if (pasteArea) {
pasteArea.addEventListener('paste', function(e) {
e.preventDefault();
const items = (e.clipboardData || window.clipboardData).items;
for (const item of items) {
if (item.type.indexOf('image') === 0) {
const blob = item.getAsFile();
const reader = new FileReader();
reader.onload = function(event) {
imagePreview.src = event.target.result;
pastedImageDataInput.value = event.target.result;
clearOtherImageInputs('paste');
};
reader.readAsDataURL(blob);
}
}
});
pasteArea.addEventListener('dragover', (e) => { e.preventDefault(); pasteArea.classList.add('hover'); });
pasteArea.addEventListener('dragleave', () => pasteArea.classList.remove('hover'));
pasteArea.addEventListener('drop', (e) => { e.preventDefault(); pasteArea.classList.remove('hover'); });
}
const consumableCheckbox = document.getElementById('consumable');
const quantityOwnedInput = document.getElementById('quantity_owned');
consumableCheckbox.addEventListener('change', function() {
if (this.checked) {
quantityOwnedInput.disabled = true;
quantityOwnedInput.value = 1;
} else {
quantityOwnedInput.disabled = false;
}
});
});
</script>
<?php require_once 'footer.php'; ?>

110
add_packing_list.php Normal file
View File

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

View File

@@ -0,0 +1,179 @@
<?php
// api_packing_list_handler.php - Verarbeitet AJAX-Anfragen von der Packlisten-Bearbeitungsseite
// FINALE, STABILE VERSION 14: Implementiert eine robuste "State-Sync"-Logik.
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json');
// Error handling...
function handle_fatal_error() {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR])) {
if (headers_sent()) { return; }
http_response_code(500);
echo json_encode(['error' => 'Kritischer Serverfehler: ' . $error['message']]);
exit;
}
}
register_shutdown_function('handle_fatal_error');
function handle_all_errors($errno, $errstr, $errfile, $errline) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler('handle_all_errors');
$conn = null;
try {
if (!isset($_SESSION['user_id'])) {
throw new Exception('Nicht angemeldet', 403);
}
require_once 'db_connect.php';
$data = json_decode(file_get_contents('php://input'), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Ungültige Anfrage: JSON konnte nicht dekodiert werden.', 400);
}
$action = $data['action'] ?? '';
$packing_list_id = intval($data['packing_list_id'] ?? 0);
if ($packing_list_id === 0) {
throw new Exception("Keine Packlisten-ID angegeben.", 400);
}
if (!user_can_edit_list($conn, $packing_list_id, $_SESSION['user_id'])) {
throw new Exception('Zugriff verweigert.', 403);
}
$conn->begin_transaction();
switch ($action) {
case 'sync_list':
$items_from_frontend = $data['list'];
$old_quantities = [];
$stmt_get_old = $conn->prepare("SELECT id, quantity FROM packing_list_items WHERE packing_list_id = ?");
$stmt_get_old->bind_param("i", $packing_list_id);
$stmt_get_old->execute();
$result_old = $stmt_get_old->get_result();
while($row = $result_old->fetch_assoc()) {
$old_quantities[$row['id']] = $row['quantity'];
}
$stmt_get_old->close();
// FIX: Erst alle Eltern-Beziehungen lösen, um Foreign-Key-Fehler beim Löschen zu vermeiden
$stmt_unlink_parents = $conn->prepare("UPDATE packing_list_items SET parent_packing_list_item_id = NULL WHERE packing_list_id = ?");
$stmt_unlink_parents->bind_param("i", $packing_list_id);
$stmt_unlink_parents->execute();
$stmt_unlink_parents->close();
$stmt_delete = $conn->prepare("DELETE FROM packing_list_items WHERE packing_list_id = ?");
$stmt_delete->bind_param("i", $packing_list_id);
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 (?, ?, ?, ?, ?, ?)");
$id_map = []; // Mappt temporäre Frontend-IDs auf neue DB-IDs
foreach ($items_from_frontend as $index => $item_data) {
$pli_id_frontend = $item_data['pli_id'];
$article_id = intval($item_data['article_id']);
$carrier_id = ($item_data['carrier_id'] === 'null') ? NULL : intval($item_data['carrier_id']);
// 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;
$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);
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;
if (!empty($item_data['is_new']) && !empty($item_data['include_children'])) {
$stmt_children = $conn->prepare("SELECT id FROM articles WHERE parent_article_id = ?");
$stmt_children->bind_param("i", $article_id);
$stmt_children->execute();
$result_children = $stmt_children->get_result();
$child_items = $result_children->fetch_all(MYSQLI_ASSOC);
$stmt_children->close();
if (!empty($child_items)) {
$temp_index = $index;
// Schiebe alle nachfolgenden Elemente nach hinten
$stmt_shift = $conn->prepare("UPDATE packing_list_items SET order_index = order_index + ? WHERE packing_list_id = ? AND order_index > ?");
$count_children = count($child_items);
$stmt_shift->bind_param("iii", $count_children, $packing_list_id, $temp_index);
$stmt_shift->execute();
$stmt_shift->close();
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);
if(!$stmt_insert->execute()) throw new Exception("Fehler beim Einfügen von Kind-Artikel " . $child['id']);
}
}
}
}
$stmt_insert->close();
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();
break;
case 'update_quantity':
$item_id = intval($data['item_id']);
$quantity = intval($data['quantity']);
if ($quantity > 0) {
$stmt = $conn->prepare("UPDATE packing_list_items SET quantity = ? WHERE id = ?");
$stmt->bind_param("ii", $quantity, $item_id); $stmt->execute(); $stmt->close();
$conn->commit(); echo json_encode(['success' => true]); exit;
} else { throw new Exception('Ungültige Mengenangabe.'); }
break;
default:
throw new Exception('Unbekannte Aktion angefordert.', 400);
}
$conn->commit();
echo json_encode(get_all_items($conn, $packing_list_id));
} catch (Exception $e) {
// Versuch Rollback, falls Transaktion aktiv war
if ($conn) {
try { $conn->rollback(); } catch (Throwable $t) {}
}
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
} finally {
if ($conn && $conn instanceof mysqli) $conn->close();
}
function user_can_edit_list($conn, $packing_list_id, $user_id) {
if (empty($packing_list_id) || empty($user_id)) return false;
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $user_id);
$stmt_household->execute();
$household_id = $stmt_household->get_result()->fetch_assoc()['household_id'] ?? null;
$stmt_household->close();
$stmt_list = $conn->prepare("SELECT user_id, household_id FROM packing_lists WHERE id = ?");
$stmt_list->bind_param("i", $packing_list_id);
$stmt_list->execute();
$list_data_result = $stmt_list->get_result();
$stmt_list->close();
if ($list_data_result->num_rows === 0) return false;
$list_data = $list_data_result->fetch_assoc();
if ($list_data['user_id'] == $user_id) return true;
if (!empty($list_data['household_id']) && $list_data['household_id'] == $household_id) return true;
return false;
}
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->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt->close();
return $result;
}
?>

392
articles.php Normal file
View File

@@ -0,0 +1,392 @@
<?php
// articles.php - Zeigt alle verfügbaren Artikel an
// FINALE VERSION: Gruppiert nach Kategorien, Sticky Header, Accordion
$page_title = "Artikel im Haushalt";
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 'header.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
// Lade Haushalts-ID und Mitglieder-IDs
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$current_user_household_id = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
$household_member_ids = [$current_user_id];
if ($current_user_household_id) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $current_user_household_id);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) {
if (!in_array($row['id'], $household_member_ids)) {
$household_member_ids[] = $row['id'];
}
}
$stmt_members->close();
}
// Löschlogik
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['delete_article_id'])) {
$article_to_delete_id = intval($_POST['delete_article_id']);
$stmt_get_article = $conn->prepare("SELECT name, user_id, household_id FROM articles WHERE id = ?");
$stmt_get_article->bind_param("i", $article_to_delete_id);
$stmt_get_article->execute();
$article_to_delete = $stmt_get_article->get_result()->fetch_assoc();
$stmt_get_article->close();
$can_delete = false;
if ($article_to_delete) {
$is_owner = ($article_to_delete['user_id'] == $current_user_id);
$is_household_article = !empty($article_to_delete['household_id']) && $article_to_delete['household_id'] == $current_user_household_id;
if ($is_owner || $is_household_article) {
$can_delete = true;
}
}
if ($can_delete) {
$stmt_delete = $conn->prepare("DELETE FROM articles WHERE id = ?");
$stmt_delete->bind_param("i", $article_to_delete_id);
if ($stmt_delete->execute()) {
if ($current_user_household_id) {
$log_message = htmlspecialchars($_SESSION['username']) . " hat den Artikel '" . htmlspecialchars($article_to_delete['name']) . "' gelöscht.";
log_household_action($conn, $current_user_household_id, $current_user_id, $log_message);
}
$message = '<div class="alert alert-success" role="alert">Artikel erfolgreich gelöscht.</div>';
} else {
$message = '<div class="alert alert-danger" role="alert">Fehler beim Löschen des Artikels: ' . $stmt_delete->error . '</div>';
}
$stmt_delete->close();
} else {
$message = '<div class="alert alert-danger" role="alert">Sie sind nicht berechtigt, diesen Artikel zu löschen.</div>';
}
}
$articles = [];
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
$sql = "SELECT
a.id, a.name, a.weight_grams, a.quantity_owned, a.product_url, a.consumable, a.image_url, a.user_id, a.parent_article_id,
u.username as creator_name, a.household_id, a.product_designation,
c.id AS category_id, c.name AS category_name,
m.id AS manufacturer_id, m.name AS manufacturer_name,
l2.name AS location_level2_name, l1.name AS location_level1_name
FROM articles a
JOIN users u ON a.user_id = u.id
LEFT JOIN categories c ON a.category_id = c.id
LEFT JOIN manufacturers m ON a.manufacturer_id = m.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 a.user_id IN ($placeholders) OR a.household_id = ?
ORDER BY c.name ASC, a.name ASC"; // Pre-sort by category helps, but JS does the grouping
$stmt = $conn->prepare($sql);
if ($stmt === false) {
$message .= '<div class="alert alert-danger" role="alert">SQL Prepare-Fehler (Artikel laden): ' . $conn->error . '</div>';
} else {
$all_params = array_merge($household_member_ids, [$current_user_household_id]);
$all_types = $types . 'i';
$stmt->bind_param($all_types, ...$all_params);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
$articles[] = $row;
}
$stmt->close();
}
$stmt_cat_load = $conn->prepare("SELECT id, name FROM categories WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_cat_load->bind_param($types, ...$household_member_ids);
$stmt_cat_load->execute();
$categories_for_filter = $stmt_cat_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_cat_load->close();
$stmt_man_load = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_man_load->bind_param($types, ...$household_member_ids);
$stmt_man_load->execute();
$manufacturers_for_filter = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_man_load->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-boxes me-2"></i>Artikel im Haushalt</h2>
<a href="add_article.php" class="btn btn-sm btn-outline-light"><i class="fas fa-plus me-2"></i>Neuen Artikel hinzufügen</a>
</div>
<div class="card-body p-0">
<?php echo $message; ?>
<div class="filter-controls p-3 border-bottom bg-light">
<div class="row g-2">
<div class="col-md-5"><input type="text" id="filter-text" class="form-control form-control-sm" placeholder="Suchen..."></div>
<div class="col-md-3"><select id="filter-category" class="form-select form-select-sm"><option value="">Alle Kategorien</option><?php foreach($categories_for_filter as $cat) echo '<option value="'.$cat['id'].'">'.htmlspecialchars($cat['name']).'</option>'; ?></select></div>
<div class="col-md-4"><select id="filter-manufacturer" class="form-select form-select-sm"><option value="">Alle Hersteller</option><?php foreach($manufacturers_for_filter as $man) echo '<option value="'.$man['id'].'">'.htmlspecialchars($man['name']).'</option>'; ?></select></div>
</div>
</div>
<div class="table-responsive" style="max-height: 75vh;">
<table class="table table-hover mb-0">
<thead>
<tr>
<th style="width: 60px;">Bild</th>
<th style="width: 40px;"><i class="fas fa-link"></i></th>
<th>Name</th>
<th>Hersteller</th>
<th>Modell/Typ</th>
<th>Gewicht</th>
<th>Anzahl</th>
<th>Kategorie</th>
<th>Lagerort</th>
<th>Besitzer</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="articlesTableBody">
</tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="imageModal" tabindex="-1" aria-labelledby="imageModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageModalLabel">Bildansicht</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img src="" id="fullImage" class="modal-image" alt="Vollbildansicht">
</div>
</div>
</div>
</div>
<div id="image-preview-tooltip" class="image-preview-tooltip"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const articlesData = <?php echo json_encode($articles); ?>;
const currentUserId = <?php echo $current_user_id; ?>;
const currentUserHouseholdId = <?php echo json_encode($current_user_household_id); ?>;
const collapseDefault = <?php echo isset($_SESSION['articles_collapse_default']) && $_SESSION['articles_collapse_default'] == '1' ? 'true' : 'false'; ?>;
const articlesById = {};
// ... (Rest of data prep) ...
articlesData.forEach(article => {
articlesById[article.id] = { ...article, children: [] };
});
const articlesHierarchical = [];
Object.values(articlesById).forEach(article => {
if (article.parent_article_id && articlesById[article.parent_article_id]) {
articlesById[article.parent_article_id].children.push(article);
} else {
articlesHierarchical.push(article);
}
});
const tableBody = document.getElementById('articlesTableBody');
const filterText = document.getElementById('filter-text');
const filterCategory = document.getElementById('filter-category');
const filterManufacturer = document.getElementById('filter-manufacturer');
const collapsedCategories = new Set();
// Initialize collapsed state based on user setting
if (collapseDefault) {
// We need to know all category names to collapse them initially
// We can derive them from data or just handle it dynamically in renderTable,
// but for cleanest initial state, let's populate the Set.
const uniqueCategories = new Set(articlesData.map(a => a.category_name || 'Ohne Kategorie'));
uniqueCategories.forEach(c => collapsedCategories.add(c));
}
function renderTable() {
const textValue = filterText.value.toLowerCase();
const categoryValue = filterCategory.value;
const manufacturerValue = filterManufacturer.value;
const isSearching = textValue.length > 0 || categoryValue || manufacturerValue;
function articleMatchesFilter(article) {
const matchesText = article.name.toLowerCase().includes(textValue) ||
(article.manufacturer_name && article.manufacturer_name.toLowerCase().includes(textValue)) ||
(article.product_designation && article.product_designation.toLowerCase().includes(textValue));
const matchesCategory = !categoryValue || article.category_id == categoryValue;
const matchesManufacturer = !manufacturerValue || article.manufacturer_id == manufacturerValue;
return matchesText && matchesCategory && matchesManufacturer;
}
const filteredList = articlesHierarchical.filter(article => {
if (articleMatchesFilter(article)) return true;
if (article.children.length > 0) return article.children.some(child => articleMatchesFilter(child));
return false;
});
const groupedArticles = {};
filteredList.forEach(article => {
const catName = article.category_name || 'Ohne Kategorie';
if (!groupedArticles[catName]) groupedArticles[catName] = [];
groupedArticles[catName].push(article);
});
const sortedCategories = Object.keys(groupedArticles).sort((a, b) => {
if (a === 'Ohne Kategorie') return 1;
if (b === 'Ohne Kategorie') return -1;
return a.localeCompare(b);
});
let tableHTML = '';
if (sortedCategories.length === 0) {
tableHTML = '<tr><td colspan="12" class="text-center text-muted p-4">Keine Artikel gefunden.</td></tr>';
} else {
sortedCategories.forEach(catName => {
const items = groupedArticles[catName];
// If searching, expand all. If not, respect collapsed state.
const isCollapsed = collapsedCategories.has(catName) && !isSearching;
const count = items.reduce((acc, item) => acc + 1 + item.children.length, 0);
tableHTML += `<tr class="category-header ${isCollapsed ? 'collapsed' : ''}" data-category="${catName}">
<td colspan="12">
<i class="fas fa-chevron-down me-2"></i> ${catName}
<span class="badge bg-light text-dark rounded-pill ms-2">${count}</span>
</td>
</tr>`;
if (!isCollapsed) {
items.forEach(article => {
tableHTML += generateRowHTML(article);
});
}
});
}
tableBody.innerHTML = tableHTML;
initializeInteractivity();
}
function generateRowHTML(article, level = 0) {
let html = '';
const imagePath = article.image_url ? article.image_url : 'keinbild.png';
const productLink = article.product_url ? `<a href="${article.product_url}" target="_blank" class="btn btn-sm btn-outline-secondary" data-bs-toggle="tooltip" title="Produktseite öffnen"><i class="fas fa-external-link-alt"></i></a>` : '';
const creatorBadge = article.user_id != currentUserId ? `<span class="badge bg-info">${article.creator_name}</span>` : `<span class="badge bg-primary">Ich</span>`;
const householdBadge = article.household_id ? `<span class="badge bg-success" data-bs-toggle="tooltip" title="Für den Haushalt freigegeben"><i class="fas fa-users"></i></span>` : `<span class="badge bg-secondary" data-bs-toggle="tooltip" title="Privater Artikel"><i class="fas fa-user"></i></span>`;
const consumableIcon = article.consumable == 1 ? ` <i class="fas fa-cookie-bite text-warning" data-bs-toggle="tooltip" title="Verbrauchsartikel"></i>` : '';
const quantityBadge = article.consumable == 1 ? `<span class="badge bg-secondary rounded-pill" data-bs-toggle="tooltip" title="Verbrauchsartikel">&infin;</span>` : `<span class="badge bg-secondary rounded-pill">${article.quantity_owned}</span>`;
let actionButtons = '';
const isOwner = (article.user_id == currentUserId);
const isHouseholdArticle = (article.household_id && article.household_id == currentUserHouseholdId);
if (isOwner || isHouseholdArticle) {
actionButtons = `<div class="btn-group">
<a href="edit_article.php?id=${article.id}" class="btn btn-sm btn-outline-primary" data-bs-toggle="tooltip" title="Bearbeiten"><i class="fas fa-edit"></i></a>`;
actionButtons += `<form action="articles.php" method="post" class="d-inline" onsubmit="return confirm('Sind Sie sicher?');"><input type="hidden" name="delete_article_id" value="${article.id}"><button type="submit" class="btn btn-sm btn-outline-danger" data-bs-toggle="tooltip" title="Löschen"><i class="fas fa-trash"></i></button></form>`;
actionButtons += `</div>`;
}
const indentStyle = level > 0 ? `padding-left: ${1.5 * level}rem;` : '';
const treePrefix = level > 0 ? `<span class="tree-line" style="left: ${0.5 * level}rem;"></span>` : '';
const nameCellContent = `<div class="tree-view-item level-${level}" style="position:relative;">${treePrefix}<span class="item-name-text">${article.name}</span></div>`;
html += `<tr>
<td><img src="${imagePath}" alt="${article.name}" style="width: 40px; height: 40px; object-fit: cover; border-radius: 5px;" class="article-image-trigger" data-preview-url="${imagePath}"></td>
<td class="text-center">${productLink}</td>
<td>${nameCellContent}</td>
<td>${article.manufacturer_name || '---'}</td>
<td>${article.product_designation || '---'}</td>
<td>${new Intl.NumberFormat('de-DE').format(article.weight_grams)} g</td>
<td>${quantityBadge}</td>
<td>${article.category_name || '---'}</td>
<td>${article.location_level1_name ? `<small>${article.location_level1_name} <i class="fas fa-chevron-right fa-xs"></i> ${article.location_level2_name}</small>` : '---'}</td>
<td>${creatorBadge}</td>
<td>${householdBadge}${consumableIcon}</td>
<td>${actionButtons}</td>
</tr>`;
if (article.children.length > 0) {
article.children.forEach(child => {
html += generateRowHTML(child, level + 1);
});
}
return html;
}
const tooltip = document.getElementById('image-preview-tooltip');
const imageModal = new bootstrap.Modal(document.getElementById('imageModal'));
const fullImageElement = document.getElementById('fullImage');
const imageModalLabel = document.getElementById('imageModalLabel');
function initializeInteractivity() {
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
if (tooltip) {
document.querySelectorAll('.article-image-trigger').forEach(trigger => {
trigger.addEventListener('mouseover', function (e) {
const previewUrl = this.getAttribute('data-preview-url');
if(previewUrl && !previewUrl.endsWith('keinbild.png')) {
tooltip.style.backgroundImage = `url('${previewUrl}')`;
tooltip.style.display = 'block';
}
});
trigger.addEventListener('mousemove', function (e) {
tooltip.style.left = e.pageX + 15 + 'px';
tooltip.style.top = e.pageY + 15 + 'px';
});
trigger.addEventListener('mouseout', function (e) {
tooltip.style.display = 'none';
});
trigger.addEventListener('click', function(e) {
const fullImageUrl = this.getAttribute('data-preview-url');
const articleName = this.getAttribute('alt');
if(fullImageUrl && !fullImageUrl.endsWith('keinbild.png')) {
fullImageElement.src = fullImageUrl;
imageModalLabel.textContent = articleName;
imageModal.show();
}
});
});
}
document.querySelectorAll('.category-header').forEach(header => {
header.addEventListener('click', function() {
const catName = this.getAttribute('data-category');
if (collapsedCategories.has(catName)) {
collapsedCategories.delete(catName);
} else {
collapsedCategories.add(catName);
}
renderTable();
});
});
}
filterText.addEventListener('input', renderTable);
filterCategory.addEventListener('change', renderTable);
filterManufacturer.addEventListener('change', renderTable);
renderTable();
});
</script>
<?php require_once 'footer.php'; ?>

886
assets/css/style.css Normal file
View File

@@ -0,0 +1,886 @@
:root {
/* Color Palette - More Vibrant */
--color-primary: #2e7d32; /* Richer Green */
--color-primary-light: #4caf50;
--color-primary-dark: #1b5e20;
--color-secondary: #546e7a;
--color-accent: #00e676; /* Pop Green */
--color-background: #f0f2f5;
/* Glassmorphism Variables - Enhanced */
--glass-bg: rgba(255, 255, 255, 0.75); /* More transparent */
--glass-border: 1px solid rgba(255, 255, 255, 0.6);
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
--glass-blur: blur(12px); /* Stronger blur */
/* Text Colors */
--text-main: #1a1a1a;
--text-muted: #555;
--text-light: #ffffff;
/* Spacing & Radius */
--border-radius-lg: 20px; /* Rounder */
--border-radius-md: 15px;
--border-radius-sm: 10px;
}
/* Robust Reset */
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: 'Nunito Sans', sans-serif;
color: var(--text-main);
background-color: var(--color-background);
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
overflow-x: hidden;
overflow-y: auto;
}
h1, h2, h3, h4, h5, h6, .navbar-brand {
font-family: 'Poppins', sans-serif; /* Modern Headings */
font-weight: 600;
letter-spacing: -0.5px;
}
/* Wallpaper Background with Overlay */
body::before {
content: "";
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background-image: linear-gradient(to bottom right, rgba(46, 125, 50, 0.1), rgba(0, 0, 0, 0.2)), url('../../wallpaper.jpg'); /* Gradient Overlay */
background-size: cover;
background-position: center center;
background-attachment: fixed;
z-index: -1;
}
/* Layout: Sidebar & Main Content */
.page-wrapper {
display: flex;
width: 100%;
min-height: 100vh;
}
/* Sidebar Styling - Modern Dark Glass */
.sidebar {
width: 280px;
flex-shrink: 0;
background: rgba(20, 40, 20, 0.85); /* Dark semi-transparent */
backdrop-filter: blur(15px);
border-right: 1px solid rgba(255,255,255,0.1);
color: var(--text-light);
padding: 25px;
display: flex;
flex-direction: column;
box-shadow: 10px 0 30px rgba(0,0,0,0.1);
z-index: 100;
min-height: 100vh;
}
.sidebar .navbar-brand {
color: var(--text-light);
margin-bottom: 40px;
text-align: center;
text-decoration: none;
display: block;
text-transform: uppercase;
font-size: 1.1rem;
}
.sidebar .navbar-brand-logo {
width: 90px;
height: 90px;
margin-bottom: 15px;
border-radius: 25px; /* Squircle */
object-fit: cover;
border: 2px solid rgba(255,255,255,0.5);
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
transition: transform 0.3s ease;
}
.sidebar .navbar-brand:hover .navbar-brand-logo {
transform: rotate(-3deg) scale(1.05);
}
.sidebar .nav-link {
color: rgba(255, 255, 255, 0.7);
padding: 14px 20px;
border-radius: var(--border-radius-md);
transition: all 0.3s ease;
margin-bottom: 8px;
display: flex;
align-items: center;
text-decoration: none;
font-weight: 500;
font-family: 'Poppins', sans-serif;
}
.sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.15);
color: var(--text-light);
transform: translateX(5px);
}
.sidebar .nav-link.active {
background: linear-gradient(90deg, var(--color-primary-light), var(--color-primary));
color: var(--text-light);
font-weight: 600;
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.4);
}
/* Modern Card Design (Glassmorphism) */
.card {
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border: var(--glass-border);
border-radius: var(--border-radius-lg);
box-shadow: var(--glass-shadow);
margin-bottom: 2rem;
overflow: hidden;
transition: transform 0.2s ease;
}
/* Subtle hover lift for interactive cards */
.card.packing-list-card:hover {
transform: translateY(-5px);
}
.card-header {
background-color: rgba(255, 255, 255, 0.5);
color: var(--color-primary-dark);
border-bottom: 1px solid rgba(0,0,0,0.05);
padding: 1.5rem;
font-weight: 600;
font-family: 'Poppins', sans-serif;
}
/* Fix for buttons inside card header being invisible if they are outline-light */
.card-header .btn-outline-light {
color: var(--color-primary);
border-color: var(--color-primary);
}
.card-header .btn-outline-light:hover {
background-color: var(--color-primary);
color: white;
}
.card-body {
padding: 1.5rem;
}
/* Buttons */
.btn-primary {
background-color: var(--color-primary-light);
border-color: var(--color-primary-light);
border-radius: var(--border-radius-sm);
padding: 0.5rem 1.2rem;
font-weight: 600;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: var(--color-primary);
border-color: var(--color-primary);
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.btn-outline-primary {
color: var(--color-primary-light);
border-color: var(--color-primary-light);
}
.btn-outline-primary:hover {
background-color: var(--color-primary-light);
border-color: var(--color-primary-light);
color: #fff;
}
/* Text Colors Override */
.text-primary {
color: var(--color-primary) !important;
}
.btn-outline-light {
border-color: rgba(255,255,255,0.5);
color: rgba(255,255,255,0.8);
}
.btn-outline-light:hover {
background-color: rgba(255,255,255,0.2);
color: #fff;
border-color: #fff;
}
/* Form Switch / Checkbox Override */
.form-check-input:checked {
background-color: var(--color-primary-light);
border-color: var(--color-primary-light);
}
.form-check-input:focus {
border-color: var(--color-primary-light);
box-shadow: 0 0 0 0.25rem rgba(76, 175, 80, 0.25);
}
/* Login & Register specific styles */
.auth-wrapper {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.auth-container {
max-width: 420px;
width: 100%;
border-radius: var(--border-radius-lg);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
background-color: rgba(255, 255, 255, 0.95);
overflow: hidden;
backdrop-filter: blur(10px);
}
.auth-header {
background-color: var(--color-primary);
color: white;
padding: 30px;
text-align: center;
}
.auth-logo {
width: 100px;
height: 100px;
border-radius: 50%;
border: 4px solid rgba(255,255,255,0.3);
margin-bottom: 15px;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}
.auth-body {
padding: 40px 30px;
}
/* Charts & Dashboard */
.chart-container {
position: relative;
height: 300px;
width: 100%;
}
/* Welcome Section (Dashboard) */
.welcome-section {
text-align: center;
padding: 40px 0;
}
.welcome-section h1 {
font-size: 2.5rem;
margin-bottom: 15px;
font-weight: 700;
color: var(--color-primary);
}
.welcome-section p.lead {
font-size: 1.2rem;
color: var(--text-muted);
}
.welcome-image-container {
max-width: 800px;
height: 350px;
margin: 40px auto;
border-radius: var(--border-radius-lg);
overflow: hidden;
box-shadow: 0 15px 35px rgba(0,0,0,0.15);
}
.animated-bg {
width: 100%;
height: 100%;
background-image: url('../../wallpaper.jpg');
background-size: cover;
background-position: center center;
animation: ken-burns 25s infinite alternate ease-in-out;
}
@keyframes ken-burns {
0% { transform: scale(1) translate(0, 0); }
100% { transform: scale(1.15) translate(-2%, 2%); }
}
/* Packing List Cards */
.packing-list-card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
background-color: rgba(255, 255, 255, 0.6); /* Slightly more opaque than standard card */
border-left: 5px solid var(--color-primary-light); /* Accent border */
}
.packing-list-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 40px rgba(0,0,0,0.15) !important;
}
.packing-list-card .card-body {
position: relative;
}
.packing-list-card .card-icon {
position: absolute;
top: 1rem;
right: 1rem;
width: 60px;
height: 60px;
pointer-events: none;
opacity: 0.15;
filter: grayscale(100%);
transition: all 0.3s ease;
}
.packing-list-card:hover .card-icon {
opacity: 0.4;
transform: rotate(10deg) scale(1.1);
filter: grayscale(0%);
}
.packing-list-card .card-title {
font-weight: 700;
color: var(--color-primary-dark);
margin-bottom: 0.5rem;
}
.packing-list-card .card-footer {
background-color: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0,0,0,0.05);
padding: 1rem 1.5rem;
}
.creator-badge {
font-size: 0.75em;
font-weight: 600;
opacity: 0.9;
}
/* Footer */
.main-footer {
background-color: transparent; /* Transparent to blend with glassmorphism or simple text */
color: var(--text-muted);
text-align: center;
padding: 1.5rem;
font-size: 0.85rem;
margin-top: auto; /* Push to bottom if in flex container */
border-top: 1px solid rgba(0,0,0,0.05);
}
/* Ensure main content pushes footer down */
.main-content {
flex-grow: 1;
padding: 30px;
display: flex;
flex-direction: column;
}
/* --- Styles from articles.php --- */
.image-preview-tooltip {
display: none;
position: absolute;
border: 1px solid rgba(0,0,0,0.1);
background-color: #fff;
padding: 5px;
z-index: 1056;
width: 150px;
height: 150px;
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
border-radius: var(--border-radius-sm);
pointer-events: none;
}
.table tbody td {
vertical-align: middle !important;
}
.article-image-trigger {
cursor: pointer;
transition: transform 0.2s;
}
.article-image-trigger:hover {
transform: scale(1.1);
}
.modal-image {
max-width: 100%;
max-height: 80vh;
border-radius: var(--border-radius-sm);
}
/* --- Styles from add_article.php --- */
.section-card {
border: 1px solid rgba(0,0,0,0.08);
border-radius: var(--border-radius-md);
overflow: hidden; /* Ensure header radius respects container */
background-color: #fff;
}
.section-card .card-header {
background-color: rgba(59, 74, 35, 0.08); /* Very light primary */
color: var(--color-primary-dark);
border-bottom: 1px solid rgba(0,0,0,0.05);
font-weight: 700;
}
.section-card .card-body {
background-color: #fff;
}
.paste-area {
border: 2px dashed var(--color-secondary);
border-radius: var(--border-radius-sm);
padding: 2rem 1rem;
text-align: center;
color: var(--text-muted);
background-color: #fafafa;
transition: all 0.2s;
cursor: pointer;
}
.paste-area.hover, .paste-area:hover {
background-color: rgba(59, 74, 35, 0.1);
border-color: var(--color-primary-light);
color: var(--color-primary);
}
#imagePreview {
max-width: 100%;
max-height: 250px;
border-radius: var(--border-radius-sm);
object-fit: contain;
border: 1px solid rgba(0,0,0,0.1);
padding: 5px;
background-color: #fff;
}
/* --- Packing List Editor Styles --- */
.editor-container {
display: grid;
grid-template-columns: 40% 1fr;
gap: 1.5rem;
}
.editor-pane {
display: flex;
flex-direction: column;
height: 75vh;
background-color: #ffffff;
border: 1px solid rgba(0,0,0,0.1);
border-radius: var(--border-radius-md);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.pane-header {
padding: 1rem;
flex-shrink: 0;
border-bottom: 1px solid rgba(0,0,0,0.05);
background-color: rgba(248, 249, 250, 0.5);
}
.pane-content {
overflow-y: auto;
flex-grow: 1;
padding: 1rem;
}
.available-item-card {
padding: 8px 12px;
margin-bottom: 6px;
background: #f8f9fa;
border-radius: var(--border-radius-sm);
cursor: grab;
border: 1px solid rgba(0,0,0,0.05);
transition: background-color 0.2s;
}
.available-item-card:hover {
background-color: #e9ecef;
}
.carrier-box {
margin-bottom: 1.5rem;
border: 1px solid rgba(0,0,0,0.1);
border-radius: var(--border-radius-sm);
overflow: hidden;
}
.carrier-header {
padding: 0.75rem 1.25rem;
background-color: rgba(233, 236, 239, 0.5);
border-bottom: 1px solid rgba(0,0,0,0.05);
font-weight: 600;
}
.carrier-list {
min-height: 80px;
padding: 10px;
background-color: #fff;
}
.packed-item-row {
display: flex;
align-items: center;
padding: 8px;
margin-bottom: 4px;
border-bottom: 1px solid rgba(0,0,0,0.03);
background-color: #fff;
border-radius: var(--border-radius-sm);
transition: background-color 0.2s;
}
.packed-item-row:hover {
background-color: #fafafa;
}
.packed-item-row.ghost-class {
background-color: rgba(59, 74, 35, 0.1);
border: 1px dashed var(--color-primary);
opacity: 0.7;
}
.handle {
cursor: grab;
color: var(--color-secondary);
margin: 0 10px 0 5px;
}
.item-name {
flex-grow: 1;
font-size: 0.95rem;
}
.item-controls {
display: flex;
align-items: center;
gap: 5px;
}
.quantity-input {
width: 70px;
text-align: center;
}
.nesting-level-1 {
margin-left: 30px !important;
border-left: 3px solid rgba(0,0,0,0.1);
}
.nesting-level-2 {
margin-left: 60px !important;
border-left: 3px solid rgba(0,0,0,0.1);
}
/* --- Storage Locations Styles --- */
.level-1-card {
border: 1px solid rgba(0,0,0,0.1);
border-radius: var(--border-radius-md);
overflow: hidden;
}
.level-1-card .card-header {
background-color: rgba(59, 74, 35, 0.1);
color: var(--color-primary-dark);
border-bottom: 1px solid rgba(0,0,0,0.05);
font-weight: 700;
}
.level-1-card .card-body {
background-color: rgba(255, 255, 255, 0.6); /* Slightly transparent */
}
.level-2-list .list-group-item {
background-color: rgba(255, 255, 255, 0.8);
border-left: 4px solid var(--color-primary-light);
margin-bottom: 2px;
border-radius: var(--border-radius-sm);
}
/* --- Tree View for Packing List Details --- */
.tree-view-item {
position: relative;
display: flex;
align-items: center;
height: 100%;
}
.tree-view-item.level-0 {
font-weight: 600;
color: var(--color-primary-dark);
}
/* Indentation Levels */
.tree-view-item.level-1 { padding-left: 1.5rem; }
.tree-view-item.level-2 { padding-left: 3rem; }
.tree-view-item.level-3 { padding-left: 4.5rem; }
/* The "L" shaped connector line for children */
.tree-view-item:not(.level-0) .tree-line {
position: absolute;
left: 0;
/* Logic for L-shape relative to the current cell */
/* Since we can't connect to the row above easily in a simple table,
we draw a small 'L' that implies structure */
width: 15px;
height: 25px; /* Roughly half row height */
border-left: 2px solid rgba(0,0,0,0.15);
border-bottom: 2px solid rgba(0,0,0,0.15);
border-bottom-left-radius: 5px; /* Smooth curve */
top: -12px; /* Move up to simulate connection to parent roughly */
display: block;
}
/* Adjust L-shape position based on level if needed,
but padding-left on parent handles the text offset.
We need the line to be at the correct X offset. */
.tree-view-item.level-1 .tree-line { left: 0.5rem; }
.tree-view-item.level-2 .tree-line { left: 2rem; }
.tree-view-item.level-3 .tree-line { left: 3.5rem; }
.item-name-text {
margin-left: 8px;
/* Ensure text doesn't overlap line */
}
/* Muted weight for cleaner look */
.table td .text-muted.small {
font-size: 0.85em;
color: #888 !important;
}
/* Better badges in table */
.badge.rounded-pill {
font-weight: 500;
letter-spacing: 0.5px;
}
/* --- Packing List Detail View Styles --- */
.item-image {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: var(--border-radius-sm);
cursor: pointer;
border: 1px solid rgba(0,0,0,0.1);
}
.card-header-stats {
background-color: var(--color-primary-light) !important;
color: var(--text-light) !important;
}
.chart-container {
position: relative;
min-height: 220px;
width: 100%;
margin-bottom: 1rem;
}
.carrier-header-row td {
background-color: rgba(0,0,0,0.03);
font-weight: 600;
padding: 1rem 1.5rem;
border-top: 1px solid rgba(0,0,0,0.05);
color: var(--color-primary-dark);
font-family: 'Poppins', sans-serif;
}
/* Remove border for the very first header to avoid gap at the top of the card */
.table tbody tr:first-child.carrier-header-row td {
border-top: none;
}
.stats-table-container {
border: 1px solid rgba(0,0,0,0.05);
border-radius: var(--border-radius-sm);
background-color: rgba(255,255,255,0.5);
overflow: hidden;
padding: 0.75rem;
}
.toggle-tree-btn {
color: var(--color-secondary);
transition: color 0.2s;
}
/* --- Articles Table Styles --- */
/* Sticky Header */
.table-responsive thead th {
position: sticky;
top: 0;
background-color: #fff; /* Opaque background to cover scrolling content */
z-index: 10; /* Above content */
box-shadow: 0 2px 2px -1px rgba(0,0,0,0.1);
}
/* Category Header Row */
.category-header {
background-color: var(--color-primary-light);
color: #fff;
cursor: pointer;
font-weight: 600;
font-family: 'Poppins', sans-serif;
}
.category-header:hover {
filter: brightness(95%);
}
.category-header i {
transition: transform 0.2s;
}
.category-header.collapsed i {
transform: rotate(-90deg);
}
.category-header td {
background-color: var(--color-primary-light) !important; /* Override bootstrap stripes */
color: white;
}
/* --- Print Styles --- */
.print-view { display: none; }
@media print {
@page { size: A4; margin: 1.5cm; }
html, body {
width: 210mm;
height: auto;
font-size: 10pt;
background: #fff;
color: #000;
-webkit-print-color-adjust: exact !important; /* Chrome, Safari */
print-color-adjust: exact !important; /* Firefox */
}
.screen-view, .print-hide, .sidebar, .main-footer, .navbar, .btn {
display: none !important;
}
.print-view {
display: block !important;
}
.print-header {
text-align: center;
margin-bottom: 10mm;
border-bottom: 2px solid #000;
padding-bottom: 5mm;
}
.print-header h1 {
font-size: 20pt;
margin: 0 0 2mm 0;
color: #000;
}
.print-header p {
font-size: 11pt;
color: #444;
margin: 0;
}
.print-carrier-group {
page-break-inside: avoid;
margin-top: 8mm;
}
.print-carrier-header {
font-size: 13pt;
background-color: #eee;
padding: 2mm 4mm;
margin-bottom: 0;
border-bottom: 1px solid #ccc;
font-weight: bold;
}
.print-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 5mm;
}
.print-table th {
text-align: left;
padding: 2mm;
border-bottom: 1px solid #000;
font-weight: bold;
font-size: 9pt;
text-transform: uppercase;
}
.print-table td {
padding: 2mm;
border-bottom: 1px solid #ddd;
vertical-align: top;
}
.print-table tr.print-child-item td {
color: #333;
}
.print-checkbox-cell {
width: 20px;
text-align: center;
}
.print-checkbox {
display: inline-block;
width: 4mm;
height: 4mm;
border: 1px solid #000;
border-radius: 1px;
}
/* Arrow for print view hierarchy */
.arrow-wrapper {
display: inline-block;
width: 4mm;
margin-right: 2mm;
border-bottom: 1px solid #999;
border-left: 1px solid #999;
height: 3mm;
vertical-align: middle;
transform: translateY(-1mm);
}
.statistics-section {
page-break-before: auto;
margin-top: 15mm;
border-top: 2px solid #000;
padding-top: 5mm;
}
.statistics-list {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4mm;
}
.statistics-list li {
border-bottom: 1px solid #eee;
padding-bottom: 1mm;
}
.statistics-list li strong {
display: block;
font-size: 9pt;
}
.statistics-list li span {
font-size: 11pt;
}
}

171
categories.php Normal file
View File

@@ -0,0 +1,171 @@
<?php
// categories.php - Kategorienverwaltung
// FINALE VERSION mit Haushaltslogik
$page_title = "Kategorien im Haushalt";
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'];
$message = '';
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$household_id = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
$household_member_ids = [$current_user_id];
if ($household_id) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $household_id);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) {
if (!in_array($row['id'], $household_member_ids)) {
$household_member_ids[] = $row['id'];
}
}
$stmt_members->close();
}
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
if ($_SERVER["REQUEST_METHOD"] == "POST") {
if (isset($_POST['add_category'])) {
$category_name = trim($_POST['category_name']);
if (!empty($category_name)) {
$stmt = $conn->prepare("INSERT INTO categories (name, user_id) VALUES (?, ?)");
$stmt->bind_param("si", $category_name, $current_user_id);
if ($stmt->execute()) {
if ($household_id) {
$log_message = htmlspecialchars($_SESSION['username']) . " hat die Kategorie '" . htmlspecialchars($category_name) . "' hinzugefügt.";
log_household_action($conn, $household_id, $current_user_id, $log_message);
}
$message = '<div class="alert alert-success" role="alert">Kategorie erfolgreich hinzugefügt!</div>';
} else { /* ... */ }
$stmt->close();
} else { /* ... */ }
}
elseif (isset($_POST['edit_category'])) {
// ...
}
}
elseif (isset($_GET['action']) && $_GET['action'] == 'delete' && isset($_GET['id'])) {
// ...
}
$stmt_load = $conn->prepare("SELECT c.id, c.name, c.user_id, u.username as creator_name FROM categories c JOIN users u ON c.user_id = u.id WHERE c.user_id IN ($placeholders) ORDER BY c.name ASC");
$stmt_load->bind_param($types, ...$household_member_ids);
$stmt_load->execute();
$categories_list = $stmt_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_load->close();
$conn->close();
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Kategorien im Haushalt</h2>
</div>
<div class="card-body p-4">
<?php if(!empty($message)) echo $message; ?>
<div class="card bg-light mb-5">
<div class="card-body">
<h5 class="card-title mb-3">Neue Kategorie hinzufügen</h5>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post" class="row g-3 align-items-end">
<div class="col-sm-8">
<label for="category_name" class="form-label visually-hidden">Neue Kategorie</label>
<input type="text" class="form-control" id="category_name" name="category_name" placeholder="Name der neuen Kategorie" required>
</div>
<div class="col-sm-4">
<button type="submit" name="add_category" class="btn btn-primary w-100"><i class="fas fa-plus-circle me-2"></i>Hinzufügen</button>
</div>
</form>
</div>
</div>
<h5 class="mb-3">Bestehende Kategorien</h5>
<?php if (empty($categories_list)): ?>
<div class="alert alert-info text-center">Keine Kategorien im Haushalt gefunden.</div>
<?php else: ?>
<div class="list-group">
<?php foreach ($categories_list as $category): ?>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-tag text-muted me-2"></i>
<span><?php echo htmlspecialchars($category['name']); ?></span>
<?php if ($category['user_id'] != $current_user_id): ?>
<small class="text-muted ms-2">(von <?php echo htmlspecialchars($category['creator_name']); ?>)</small>
<?php endif; ?>
</div>
<?php if ($category['user_id'] == $current_user_id): ?>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary" title="Bearbeiten" data-bs-toggle="modal" data-bs-target="#editCategoryModal"
data-id="<?php echo htmlspecialchars($category['id']); ?>" data-name="<?php echo htmlspecialchars($category['name']); ?>">
<i class="fas fa-edit"></i>
</button>
<a href="categories.php?action=delete&id=<?php echo htmlspecialchars($category['id']); ?>" class="btn btn-sm btn-outline-danger" title="Löschen" onclick="return confirm('Sind Sie sicher, dass Sie diese Kategorie löschen möchten? Artikel, die dieser Kategorie zugewiesen sind, verlieren ihre Zuordnung.')">
<i class="fas fa-trash-alt"></i>
</a>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="modal fade" id="editCategoryModal" tabindex="-1" aria-labelledby="editCategoryModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editCategoryModalLabel">Kategorie bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
<div class="modal-body">
<input type="hidden" name="category_id" id="edit_category_id">
<div class="mb-3">
<label for="edit_category_name" class="form-label">Kategoriename</label>
<input type="text" class="form-control" id="edit_category_name" name="category_name" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" name="edit_category" class="btn btn-primary">Änderungen speichern</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var editCategoryModal = document.getElementById('editCategoryModal');
if (editCategoryModal) {
editCategoryModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var categoryId = button.getAttribute('data-id');
var categoryName = button.getAttribute('data-name');
var modalIdInput = editCategoryModal.querySelector('#edit_category_id');
var modalNameInput = editCategoryModal.querySelector('#edit_category_name');
if (modalIdInput) modalIdInput.value = categoryId;
if (modalNameInput) modalNameInput.value = categoryName;
});
}
});
</script>
<?php require_once 'footer.php'; ?>

41
db_connect.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
// db_connect.php - Zentrale Datei für die Datenbankverbindung
// NEU: Lade Zugangsdaten aus einer sicheren Konfigurationsdatei.
// Diese Datei sollte außerhalb deines öffentlichen Web-Verzeichnisses liegen!
// z.B. in /var/www/config/packliste.ini
$config_path = __DIR__ . '/../config.ini'; // Annahme: die Datei liegt ein Verzeichnis höher
if (!file_exists($config_path)) {
die("Kritischer Fehler: Die Konfigurationsdatei wurde nicht gefunden. Bitte erstellen Sie die 'config.ini'.");
}
$config = parse_ini_file($config_path);
if ($config === false) {
die("Kritischer Fehler: Konfigurationsdatei konnte nicht gelesen werden.");
}
$servername = $config['servername'];
$username = $config['username'];
$password = $config['password'];
$dbname = $config['dbname'];
// Verbindung herstellen
$conn = new mysqli($servername, $username, $password, $dbname);
// Verbindung prüfen
if ($conn->connect_error) {
// In einer Produktivumgebung sollte der Fehler geloggt, aber nicht dem Benutzer angezeigt werden.
error_log("Datenbankverbindungsfehler: " . $conn->connect_error);
die("Verbindung zur Datenbank konnte nicht hergestellt werden. Bitte versuchen Sie es später erneut.");
}
// Zeichensatz auf UTF-8 setzen, um Probleme mit Sonderzeichen zu vermeiden
$conn->set_charset("utf8mb4");
// NEU: Setze eine Standard-Zeitzone, um konsistente Zeitstempel zu gewährleisten.
date_default_timezone_set('Europe/Berlin');
?>

69
delete_article.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
// delete_article.php - Artikel löschen
// KORREKTUR: Berechtigungsprüfung für Haushaltsmitglieder hinzugefügt.
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once 'db_connect.php';
$article_id_to_delete = isset($_GET['id']) ? intval($_GET['id']) : 0;
$current_user_id = $_SESSION['user_id'];
if ($article_id_to_delete > 0) {
// Lade Haushalts-ID des aktuellen Benutzers
$stmt_user_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_user_household->bind_param("i", $current_user_id);
$stmt_user_household->execute();
$current_user_household_id = $stmt_user_household->get_result()->fetch_assoc()['household_id'];
$stmt_user_household->close();
// Artikeldetails abrufen, um user_id und household_id zu prüfen
$stmt_check = $conn->prepare("SELECT user_id, household_id FROM articles WHERE id = ?");
$stmt_check->bind_param("i", $article_id_to_delete);
$stmt_check->execute();
$result_check = $stmt_check->get_result();
if ($result_check->num_rows == 1) {
$article = $result_check->fetch_assoc();
$can_delete = false;
$is_owner = ($article['user_id'] == $current_user_id);
$is_household_article = !empty($article['household_id']) && $article['household_id'] == $current_user_household_id;
if ($is_owner || $is_household_article) {
$can_delete = true;
}
if ($can_delete) {
$stmt_delete = $conn->prepare("DELETE FROM articles WHERE id = ?");
$stmt_delete->bind_param("i", $article_id_to_delete);
if ($stmt_delete->execute()) {
$_SESSION['message'] = '<div class="alert alert-success" role="alert">Artikel erfolgreich gelöscht!</div>';
} else {
$_SESSION['message'] = '<div class="alert alert-danger" role="alert">Fehler beim Löschen des Artikels: ' . $stmt_delete->error . '</div>';
}
$stmt_delete->close();
} else {
$_SESSION['message'] = '<div class="alert alert-danger" role="alert">Sie sind nicht berechtigt, diesen Artikel zu löschen.</div>';
}
} else {
$_SESSION['message'] = '<div class="alert alert-warning" role="alert">Artikel nicht gefunden.</div>';
}
$stmt_check->close();
} else {
$_SESSION['message'] = '<div class="alert alert-danger" role="alert">Keine Artikel-ID zum Löschen angegeben.</div>';
}
$conn->close();
// Zurück zur Artikelliste
header("Location: articles.php");
exit;
?>

56
delete_packing_list.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
// delete_packing_list.php - Packliste löschen
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
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$current_user_id = $_SESSION['user_id'];
if ($packing_list_id > 0) {
// Prüfen, ob der Benutzer berechtigt ist, diese Packliste zu löschen (nur der Ersteller)
$stmt_check = $conn->prepare("SELECT user_id, name, household_id FROM packing_lists WHERE id = ?");
$stmt_check->bind_param("i", $packing_list_id);
$stmt_check->execute();
$result_check = $stmt_check->get_result();
if ($result_check->num_rows == 1) {
$packing_list = $result_check->fetch_assoc();
if ($packing_list['user_id'] == $current_user_id) {
$stmt = $conn->prepare("DELETE FROM packing_lists WHERE id = ?");
$stmt->bind_param("i", $packing_list_id);
if ($stmt->execute()) {
if ($packing_list['household_id']) {
$log_message = htmlspecialchars($_SESSION['username']) . " hat die Packliste '" . htmlspecialchars($packing_list['name']) . "' gelöscht.";
log_household_action($conn, $packing_list['household_id'], $current_user_id, $log_message);
}
$_SESSION['message'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich gelöscht!</div>';
} else {
$_SESSION['message'] = '<div class="alert alert-danger" role="alert">Fehler beim Löschen der Packliste: ' . $stmt->error . '</div>';
}
$stmt->close();
} else {
$_SESSION['message'] = '<div class="alert alert-danger" role="alert">Sie sind nicht berechtigt, diese Packliste zu löschen.</div>';
}
} else {
$_SESSION['message'] = '<div class="alert alert-warning" role="alert">Packliste nicht gefunden.</div>';
}
$stmt_check->close();
} else {
$_SESSION['message'] = '<div class="alert alert-danger" role="alert">Keine Packlisten-ID zum Löschen angegeben.</div>';
}
$conn->close();
header("Location: packing_lists.php");
exit;
?>

128
duplicate_packing_list.php Normal file
View File

@@ -0,0 +1,128 @@
<?php
// duplicate_packing_list.php - Logik zum Duplizieren einer kompletten Packliste
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once 'db_connect.php';
$current_user_id = $_SESSION['user_id'];
$original_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if ($original_list_id <= 0) {
$_SESSION['message'] = '<div class="alert alert-danger" role="alert">Ungültige Packlisten-ID zum Duplizieren.</div>';
header("Location: packing_lists.php");
exit;
}
$conn->begin_transaction();
try {
// 1. Originale Packliste abrufen, um den Namen zu bekommen und die Berechtigung zu prüfen
$stmt_original = $conn->prepare("SELECT name, description FROM packing_lists WHERE id = ? AND user_id = ?");
$stmt_original->bind_param("ii", $original_list_id, $current_user_id);
$stmt_original->execute();
$result_original = $stmt_original->get_result();
if ($result_original->num_rows === 0) {
throw new Exception("Originale Packliste nicht gefunden oder keine Berechtigung.");
}
$original_list = $result_original->fetch_assoc();
$stmt_original->close();
// 2. Neuen Namen für die Kopie finden (z.B. "Tour (Kopie 1)", "Tour (Kopie 2)", etc.)
$base_name = $original_list['name'];
$copy_name = '';
$copy_number = 1;
while (true) {
$suffix = " (Kopie " . $copy_number . ")";
$test_name = $base_name . $suffix;
$stmt_check_name = $conn->prepare("SELECT id FROM packing_lists WHERE name = ? AND user_id = ?");
$stmt_check_name->bind_param("si", $test_name, $current_user_id);
$stmt_check_name->execute();
$stmt_check_name->store_result();
if ($stmt_check_name->num_rows === 0) {
$copy_name = $test_name;
$stmt_check_name->close();
break;
}
$stmt_check_name->close();
$copy_number++;
}
// 3. Neue Packliste in die `packing_lists` Tabelle einfügen
$stmt_new_list = $conn->prepare("INSERT INTO packing_lists (user_id, name, description) VALUES (?, ?, ?)");
$stmt_new_list->bind_param("iss", $current_user_id, $copy_name, $original_list['description']);
if (!$stmt_new_list->execute()) throw new Exception("Fehler beim Erstellen des Listenkopfeintrags.");
$new_list_id = $conn->insert_id;
$stmt_new_list->close();
// KORREKTUR: Komplette Überarbeitung der Kopierlogik
// 4. Alle Artikel aus der originalen Liste mit ihrer ID holen
$stmt_items = $conn->prepare("SELECT id, article_id, quantity, parent_packing_list_item_id, carrier_user_id FROM packing_list_items WHERE packing_list_id = ?");
$stmt_items->bind_param("i", $original_list_id);
$stmt_items->execute();
$result_items = $stmt_items->get_result();
$original_items = [];
while ($row = $result_items->fetch_assoc()) {
$original_items[] = $row;
}
$stmt_items->close();
if (!empty($original_items)) {
// 5. Hierarchie-Klonen in zwei Schritten
$old_to_new_id_map = []; // Speichert [alte_item_id] => neue_item_id
// Schritt 5a: Alle Items ohne Parent-Info kopieren und die ID-Zuordnung speichern
$stmt_insert_item = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, article_id, quantity, carrier_user_id) VALUES (?, ?, ?, ?)");
if (!$stmt_insert_item) throw new Exception("DB Prepare Fehler (Item Insert)");
foreach ($original_items as $item) {
$stmt_insert_item->bind_param("iiii", $new_list_id, $item['article_id'], $item['quantity'], $item['carrier_user_id']);
if (!$stmt_insert_item->execute()) throw new Exception("Fehler beim Kopieren von Artikel ID " . $item['article_id']);
$new_item_id = $conn->insert_id;
$old_to_new_id_map[$item['id']] = $new_item_id;
}
$stmt_insert_item->close();
// Schritt 5b: Die Parent-IDs für die neu erstellten Items aktualisieren
$stmt_update_parent = $conn->prepare("UPDATE packing_list_items SET parent_packing_list_item_id = ? WHERE id = ?");
if (!$stmt_update_parent) throw new Exception("DB Prepare Fehler (Parent Update)");
foreach ($original_items as $item) {
// Wenn das Original-Item einen Parent hatte...
if (!empty($item['parent_packing_list_item_id'])) {
$old_parent_id = $item['parent_packing_list_item_id'];
// Stelle sicher, dass der Parent auch in unserer Map existiert
if (isset($old_to_new_id_map[$old_parent_id])) {
$new_parent_id = $old_to_new_id_map[$old_parent_id];
$new_child_id = $old_to_new_id_map[$item['id']];
$stmt_update_parent->bind_param("ii", $new_parent_id, $new_child_id);
if (!$stmt_update_parent->execute()) throw new Exception("Fehler beim Setzen der Hierarchie für Item " . $new_child_id);
}
}
}
$stmt_update_parent->close();
}
$conn->commit();
$_SESSION['message'] = '<div class="alert alert-success" role="alert">Packliste erfolgreich zu "'.$copy_name.'" dupliziert!</div>';
} catch (Exception $e) {
$conn->rollback();
$_SESSION['message'] = '<div class="alert alert-danger" role="alert">Fehler beim Duplizieren: ' . $e->getMessage() . '</div>';
// NEU: Logge den tatsächlichen Fehler für den Admin
error_log("Fehler beim Duplizieren der Packliste: " . $e->getMessage());
}
header("Location: packing_lists.php");
exit;
?>

427
edit_article.php Normal file
View File

@@ -0,0 +1,427 @@
<?php
// edit_article.php - Formular zum Bearbeiten eines vorhandenen Artikels
// FINALE KORREKTUR: Alle besprochenen Fehler behoben und Komponente-Logik korrekt implementiert.
// KORREKTUR 2: SSL-Verifizierung und @-Fehlerunterdrückung korrigiert.
$page_title = "Artikel 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 'header.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
$article_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$article_data = null;
$can_edit = false;
$name = ''; $weight_grams = ''; $category_id = null; $consumable = 0; $quantity_owned = 1;
$product_url = ''; $manufacturer_id = null; $product_designation = '';
$storage_location_id = null;
$parent_article_id = null;
$display_current_image_path = 'keinbild.png';
$display_image_url_in_input = '';
$is_local_uploaded_image = false;
$upload_dir = 'uploads/images/';
if (!is_dir($upload_dir)) { @mkdir($upload_dir, 0777, true); }
function save_image_from_url($url, $upload_dir) {
// KORREKTUR: Unsichere SSL-Optionen entfernt, @-Operator entfernt
$context_options = ["http" => ["header" => "User-Agent: Mozilla/5.0\r\n", "timeout" => 10]];
$context = stream_context_create($context_options);
// Fehlerbehandlung für file_get_contents
$image_data = file_get_contents($url, false, $context);
if ($image_data === false) {
return [false, "Bild konnte von der URL nicht heruntergeladen werden. Überprüfen Sie den Link und die Erreichbarkeit."];
}
// Fehlerbehandlung für getimagesizefromstring
$image_info = getimagesizefromstring($image_data);
if ($image_info === false) {
return [false, "Die angegebene URL führt nicht zu einem gültigen Bild."];
}
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($image_info['mime'], $allowed_mime_types)) {
return [false, "Nicht unterstützter Bildtyp: " . htmlspecialchars($image_info['mime'])];
}
$extension = image_type_to_extension($image_info[2], false);
$unique_file_name = uniqid('img_url_', true) . '.' . $extension;
$destination = $upload_dir . $unique_file_name;
if (file_put_contents($destination, $image_data)) {
return [true, $destination];
}
return [false, "Bild konnte nicht auf dem Server gespeichert werden."];
}
function save_image_from_base64($base64_string, $upload_dir) {
if (preg_match('/^data:image\/(\w+);base64,/', $base64_string, $type)) {
$data = substr($base64_string, strpos($base64_string, ',') + 1);
$type = strtolower($type[1]);
if (!in_array($type, ['jpg', 'jpeg', 'png', 'gif'])) { return [false, "Nicht unterstützter Bildtyp."]; }
$data = base64_decode($data);
if ($data === false) { return [false, "Base64-Dekodierung fehlgeschlagen."]; }
} else { return [false, "Ungültiger Base64-String."]; }
$unique_file_name = uniqid('img_paste_', true) . '.' . $type;
$destination = $upload_dir . $unique_file_name;
if (file_put_contents($destination, $data)) { return [true, $destination]; }
return [false, "Bild konnte nicht gespeichert werden."];
}
$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();
$household_id_for_user = $stmt_household_check->get_result()->fetch_assoc()['household_id'];
$stmt_household_check->close();
if ($article_id > 0) {
$stmt_load_article = $conn->prepare("SELECT * FROM articles WHERE id = ?");
if ($stmt_load_article) {
$stmt_load_article->bind_param("i", $article_id);
$stmt_load_article->execute();
$result = $stmt_load_article->get_result();
if ($result->num_rows == 1) {
$article_data = $result->fetch_assoc();
$is_owner = ($article_data['user_id'] == $current_user_id);
$is_household_article = !empty($article_data['household_id']);
$is_in_same_household = ($is_household_article && $article_data['household_id'] == $household_id_for_user);
if ($is_owner || $is_in_same_household) {
$can_edit = true;
} else {
$message = '<div class="alert alert-danger" role="alert">Sie sind nicht berechtigt, diesen Artikel zu bearbeiten.</div>';
$article_data = null;
}
} else { $message = '<div class="alert alert-warning" role="alert">Artikel nicht gefunden.</div>'; }
$stmt_load_article->close();
}
}
if ($can_edit) {
$name = $article_data['name'];
$weight_grams = $article_data['weight_grams'];
$quantity_owned = $article_data['quantity_owned'];
$category_id = $article_data['category_id'];
$consumable = $article_data['consumable'];
$product_url = $article_data['product_url'] ?? '';
$manufacturer_id = $article_data['manufacturer_id'];
$product_designation = $article_data['product_designation'] ?? '';
$storage_location_id = $article_data['storage_location_id'];
$parent_article_id = $article_data['parent_article_id'];
if (!empty($article_data['image_url'])) {
$display_current_image_path = htmlspecialchars($article_data['image_url']);
if (strpos($article_data['image_url'], $upload_dir) === 0) {
$is_local_uploaded_image = true;
} else {
$display_image_url_in_input = htmlspecialchars($article_data['image_url']);
}
}
$household_member_ids = [$current_user_id];
if ($household_id_for_user) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $household_id_for_user);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) {
if (!in_array($row['id'], $household_member_ids)) { $household_member_ids[] = $row['id']; }
}
$stmt_members->close();
}
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
$stmt_cat_load = $conn->prepare("SELECT id, name FROM categories WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_cat_load->bind_param($types, ...$household_member_ids);
$stmt_cat_load->execute();
$categories = $stmt_cat_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_cat_load->close();
$stmt_man_load = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_man_load->bind_param($types, ...$household_member_ids);
$stmt_man_load->execute();
$manufacturers = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_man_load->close();
$stmt_loc_load = $conn->prepare("SELECT id, name, parent_id FROM storage_locations WHERE user_id IN ($placeholders) ORDER BY parent_id, name");
$stmt_loc_load->bind_param($types, ...$household_member_ids);
$stmt_loc_load->execute();
$all_locations = $stmt_loc_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_loc_load->close();
$storage_locations_structured = [];
foreach ($all_locations as $loc) { if ($loc['parent_id'] === NULL) { if (!isset($storage_locations_structured[$loc['id']])) { $storage_locations_structured[$loc['id']] = ['name' => $loc['name'], 'children' => []]; } } }
foreach ($all_locations as $loc) { if ($loc['parent_id'] !== NULL && isset($storage_locations_structured[$loc['parent_id']])) { $storage_locations_structured[$loc['parent_id']]['children'][] = $loc; } }
$stmt_parent_articles = $conn->prepare("SELECT a.id, a.name, m.name as manufacturer_name, a.product_designation FROM articles a LEFT JOIN manufacturers m ON a.manufacturer_id = m.id WHERE (a.user_id IN ($placeholders) OR a.household_id = ?) AND a.parent_article_id IS NULL AND a.id != ? ORDER BY a.name ASC");
$all_parent_params = array_merge($household_member_ids, [$household_id_for_user, $article_id]);
$all_parent_types = $types . 'ii';
$stmt_parent_articles->bind_param($all_parent_types, ...$all_parent_params);
$stmt_parent_articles->execute();
$parent_articles = $stmt_parent_articles->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_parent_articles->close();
}
if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
$name = trim($_POST['name']);
$weight_grams = intval($_POST['weight_grams']);
$consumable = isset($_POST['consumable']) ? 1 : 0;
$quantity_owned = ($consumable == 1) ? 1 : intval($_POST['quantity_owned']);
$product_url_for_db = !empty(trim($_POST['product_url'])) ? trim($_POST['product_url']) : NULL;
$product_designation_for_db = !empty(trim($_POST['product_designation'])) ? trim($_POST['product_designation']) : NULL;
$pasted_image_data = $_POST['pasted_image_data'] ?? '';
$image_url_from_input = trim($_POST['image_url']);
$is_household_item = isset($_POST['is_household_item']) && $household_id_for_user ? $household_id_for_user : NULL;
$storage_location_id = !empty($_POST['storage_location_id']) ? intval($_POST['storage_location_id']) : NULL;
$parent_article_id = !empty($_POST['parent_article_id']) ? intval($_POST['parent_article_id']) : NULL;
if (isset($_POST['manufacturer_id']) && $_POST['manufacturer_id'] === 'new') {
$new_manufacturer_name = trim($_POST['new_manufacturer_name']);
if (!empty($new_manufacturer_name)) {
$stmt_man = $conn->prepare("INSERT INTO manufacturers (name, user_id) VALUES (?, ?)");
$stmt_man->bind_param("si", $new_manufacturer_name, $current_user_id);
if ($stmt_man->execute()) { $manufacturer_id = $conn->insert_id; }
$stmt_man->close();
} else { $manufacturer_id = NULL; }
} else { $manufacturer_id = !empty($_POST['manufacturer_id']) ? intval($_POST['manufacturer_id']) : NULL; }
if (isset($_POST['category_id']) && $_POST['category_id'] === 'new') {
$new_category_name = trim($_POST['new_category_name']);
if (!empty($new_category_name)) {
$stmt_cat = $conn->prepare("INSERT INTO categories (name, user_id) VALUES (?, ?)");
$stmt_cat->bind_param("si", $new_category_name, $current_user_id);
if($stmt_cat->execute()){ $category_id = $conn->insert_id; }
$stmt_cat->close();
} else { $category_id = NULL; }
} else { $category_id = !empty($_POST['category_id']) ? intval($_POST['category_id']) : NULL; }
$image_url_for_db = $article_data['image_url'];
$image_error = '';
function delete_old_image_if_local($article_data, $upload_dir) {
if (!empty($article_data['image_url']) && strpos($article_data['image_url'], $upload_dir) === 0 && file_exists($article_data['image_url'])) {
@unlink($article_data['image_url']);
}
}
if (isset($_POST['delete_image_checkbox'])) {
delete_old_image_if_local($article_data, $upload_dir);
$image_url_for_db = NULL;
}
elseif (!empty($pasted_image_data)) {
delete_old_image_if_local($article_data, $upload_dir);
list($success, $result) = save_image_from_base64($pasted_image_data, $upload_dir);
if ($success) { $image_url_for_db = $result; } else { $image_error = $result; }
}
elseif (isset($_FILES['image_file']) && $_FILES['image_file']['error'] == UPLOAD_ERR_OK) {
delete_old_image_if_local($article_data, $upload_dir);
$unique_file_name = uniqid('img_', true) . '.' . strtolower(pathinfo($_FILES['image_file']['name'], PATHINFO_EXTENSION));
$destination_path = $upload_dir . $unique_file_name;
if (move_uploaded_file($_FILES['image_file']['tmp_name'], $destination_path)) {
$image_url_for_db = $destination_path;
} else { $image_error = "Fehler beim Verschieben der hochgeladenen Datei."; }
}
elseif (!empty($image_url_from_input) && $image_url_from_input !== $display_image_url_in_input) {
delete_old_image_if_local($article_data, $upload_dir);
list($success, $result) = save_image_from_url($image_url_from_input, $upload_dir);
if ($success) { $image_url_for_db = $result; } else { $image_error = $result; }
}
if (!empty($image_error)) {
$message .= '<div class="alert alert-warning" role="alert">Bild-Fehler: ' . htmlspecialchars($image_error) . ' Der Artikel wurde trotzdem aktualisiert.</div>';
}
if (empty($name) || $weight_grams < 0) {
$message .= '<div class="alert alert-danger" role="alert">Name und Gewicht sind Pflichtfelder und das Gewicht muss positiv sein.</div>';
} else {
$stmt_update = $conn->prepare("UPDATE articles SET name = ?, weight_grams = ?, quantity_owned = ?, category_id = ?, consumable = ?, image_url = ?, product_url = ?, manufacturer_id = ?, product_designation = ?, household_id = ?, storage_location_id = ?, parent_article_id = ? WHERE id = ?");
if ($stmt_update) {
$stmt_update->bind_param("siiisssisiiii", $name, $weight_grams, $quantity_owned, $category_id, $consumable, $image_url_for_db, $product_url_for_db, $manufacturer_id, $product_designation_for_db, $is_household_item, $storage_location_id, $parent_article_id, $article_id);
if ($stmt_update->execute()) {
if ($household_id_for_user) {
$log_message = htmlspecialchars($_SESSION['username']) . " hat den Artikel '" . htmlspecialchars($name) . "' bearbeitet.";
log_household_action($conn, $household_id_for_user, $current_user_id, $log_message);
}
$message .= '<div class="alert alert-success" role="alert">Artikel erfolgreich aktualisiert!</div>';
echo "<script>setTimeout(() => window.location.href = 'edit_article.php?id=$article_id', 1000);</script>";
} else {
$message .= '<div class="alert alert-danger" role="alert">Fehler beim Aktualisieren: ' . $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"><i class="fas fa-edit me-2"></i>Artikel bearbeiten</h2>
<a href="articles.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; ?>
<?php if ($article_data): ?>
<form action="edit_article.php?id=<?php echo htmlspecialchars($article_id); ?>" method="post" enctype="multipart/form-data">
<input type="hidden" name="pasted_image_data" id="pasted_image_data">
<div class="row g-4">
<div class="col-lg-7 d-flex flex-column">
<div class="card section-card mb-4">
<div class="card-header"><h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>Stammdaten</h5></div>
<div class="card-body">
<div class="mb-3"><label for="name" class="form-label">Artikelname*</label><input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($name); ?>" required></div>
<div class="row">
<div class="col-md-6 mb-3"><label for="manufacturer_id" class="form-label">Hersteller</label><select class="form-select" id="manufacturer_id" name="manufacturer_id"><option value="">-- Kein Hersteller --</option><option value="new">-- Neuen Hersteller hinzufügen --</option><?php foreach ($manufacturers as $man): ?><option value="<?php echo htmlspecialchars($man['id']); ?>" <?php if ($manufacturer_id == $man['id']) echo 'selected'; ?>><?php echo htmlspecialchars($man['name']); ?></option><?php endforeach; ?></select><div id="new_manufacturer_container" class="mt-2" style="display: none;"><input type="text" class="form-control" name="new_manufacturer_name" placeholder="Name des neuen Herstellers"></div></div>
<div class="col-md-6 mb-3"><label for="product_designation" class="form-label">Produktbezeichnung / Modell</label><input type="text" class="form-control" id="product_designation" name="product_designation" value="<?php echo htmlspecialchars($product_designation); ?>" placeholder="z.B. Forclaz MT500"></div>
</div>
<div class="mb-3">
<label for="parent_article_id" class="form-label">Ist Komponente von (optional)</label>
<select class="form-select" id="parent_article_id" name="parent_article_id">
<option value="">-- Kein Hauptartikel --</option>
<?php foreach ($parent_articles as $parent):
$parent_details = $parent['manufacturer_name'] ? htmlspecialchars($parent['manufacturer_name']) . ' - ' : '';
$parent_details .= htmlspecialchars($parent['product_designation'] ?? '');
?>
<option value="<?php echo $parent['id']; ?>" <?php if($parent_article_id == $parent['id']) echo 'selected'; ?>><?php echo htmlspecialchars($parent['name'] . ' (' . $parent_details . ')'); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="card section-card">
<div class="card-header"><h5 class="mb-0"><i class="fas fa-cogs me-2"></i>Eigenschaften & Ort</h5></div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3"><label for="weight_grams" class="form-label">Gewicht*</label><div class="input-group"><span class="input-group-text"><i class="fas fa-weight-hanging"></i></span><input type="number" class="form-control" id="weight_grams" name="weight_grams" value="<?php echo htmlspecialchars($weight_grams); ?>" required min="0"><span class="input-group-text">g</span></div></div>
<div class="col-md-6 mb-3"><label for="quantity_owned" class="form-label">Anzahl im Besitz</label><input type="number" class="form-control" id="quantity_owned" name="quantity_owned" value="<?php echo htmlspecialchars($quantity_owned); ?>" min="1" <?php if($consumable) echo 'disabled'; ?>></div>
<div class="col-12 mb-3"><label for="category_id" class="form-label">Kategorie</label><select class="form-select" id="category_id" name="category_id"><option value="">-- Keine Kategorie --</option><option value="new">-- Neue Kategorie hinzufügen --</option><?php foreach ($categories as $cat): ?><option value="<?php echo htmlspecialchars($cat['id']); ?>" <?php if ($category_id == $cat['id']) echo 'selected'; ?>><?php echo htmlspecialchars($cat['name']); ?></option><?php endforeach; ?></select><div id="new_category_container" class="mt-2" style="display: none;"><input type="text" class="form-control" name="new_category_name" placeholder="Name der neuen Kategorie"></div></div>
<div class="col-12 mb-3"><label for="storage_location_id" class="form-label">Lagerort</label><select class="form-select" id="storage_location_id" name="storage_location_id"><option value="">-- Kein fester Ort --</option><?php foreach ($storage_locations_structured as $loc1_id => $loc1_data): ?><optgroup label="<?php echo htmlspecialchars($loc1_data['name']); ?>"><?php foreach ($loc1_data['children'] as $loc2): ?><option value="<?php echo $loc2['id']; ?>" <?php if ($storage_location_id == $loc2['id']) echo 'selected'; ?>><?php echo htmlspecialchars($loc2['name']); ?></option><?php endforeach; ?></optgroup><?php endforeach; ?></select></div>
</div>
<hr class="my-3">
<div class="form-check form-switch mb-2"><input class="form-check-input" type="checkbox" role="switch" id="consumable" name="consumable" value="1" <?php echo $consumable ? 'checked' : ''; ?>><label class="form-check-label" for="consumable">Verbrauchsartikel (unbegrenzte Anzahl)</label></div>
<?php if ($household_id_for_user): ?>
<div class="form-check form-switch"><input class="form-check-input" type="checkbox" role="switch" id="is_household_item" name="is_household_item" value="1" <?php echo !empty($article_data['household_id']) ? 'checked' : ''; ?>><label class="form-check-label" for="is_household_item">Für den gesamten Haushalt freigeben</label></div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card section-card h-100">
<div class="card-header"><h5 class="mb-0"><i class="fas fa-image me-2"></i>Bild & Produktseite</h5></div>
<div class="card-body d-flex flex-column">
<div class="mb-3"><label for="product_url" class="form-label">Produktseite (URL)</label><input type="url" class="form-control" id="product_url" name="product_url" value="<?php echo htmlspecialchars($product_url); ?>" placeholder="https://..."></div>
<div class="mb-3"><label for="image_file" class="form-label">1. Neues Bild hochladen</label><input class="form-control" type="file" id="image_file" name="image_file" accept="image/*"></div>
<div class="mb-3"><label for="image_url" class="form-label">2. ODER neue Bild-URL</label><input type="url" class="form-control" id="image_url" name="image_url" value="<?php echo $display_image_url_in_input; ?>" placeholder="https://..."></div>
<div class="mb-3"><label class="form-label">3. ODER Bild einfügen (Strg+V)</label><div id="pasteArea" class="paste-area"><i class="fas fa-paste"></i> Hier klicken & einfügen</div></div>
<div class="text-center mt-auto">
<label class="form-label d-block">Vorschau</label>
<img id="imagePreview" src="<?php echo $display_current_image_path; ?>" alt="Vorschau" class="mb-2">
<?php if (!empty($article_data['image_url'])): ?>
<div class="form-check"><input type="checkbox" class="form-check-input" id="deleteImageCheckbox" name="delete_image_checkbox" value="1"><label class="form-check-label text-danger" for="deleteImageCheckbox">Aktuelles Bild löschen</label></div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-start align-items-center mt-4 border-top pt-4">
<button type="submit" class="btn btn-primary btn-lg me-3"><i class="fas fa-save me-2"></i>Änderungen speichern</button>
<a href="articles.php" class="btn btn-secondary">Abbrechen</a>
</div>
</form>
<?php elseif(empty($message)): ?>
<div class="alert alert-warning">Artikel konnte nicht geladen werden.</div>
<?php endif; ?>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
function setupAddNewOption(selectId, containerId) {
const select = document.getElementById(selectId);
const container = document.getElementById(containerId);
if (!select || !container) return;
const input = container.querySelector('input');
function toggle() {
const isNew = select.value === 'new';
container.style.display = isNew ? 'block' : 'none';
if (input) input.required = isNew;
if (!isNew && input) { input.value = ''; }
}
select.addEventListener('change', toggle);
toggle();
}
setupAddNewOption('manufacturer_id', 'new_manufacturer_container');
setupAddNewOption('category_id', 'new_category_container');
const imageFileInput = document.getElementById('image_file');
const imageUrlInput = document.getElementById('image_url');
const imagePreview = document.getElementById('imagePreview');
const pasteArea = document.getElementById('pasteArea');
const pastedImageDataInput = document.getElementById('pasted_image_data');
const deleteCheckbox = document.getElementById('deleteImageCheckbox');
const originalImageSrc = imagePreview.src;
function clearOtherImageInputs(source) {
if (source !== 'file' && imageFileInput) imageFileInput.value = '';
if (source !== 'url' && imageUrlInput) imageUrlInput.value = '';
if (source !== 'paste' && pastedImageDataInput) pastedImageDataInput.value = '';
if (deleteCheckbox) deleteCheckbox.checked = false;
}
if(imageFileInput) {
imageFileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = (e) => { imagePreview.src = e.target.result; };
reader.readAsDataURL(this.files[0]);
clearOtherImageInputs('file');
}
});
}
if (pasteArea) {
pasteArea.addEventListener('paste', function(e) {
e.preventDefault();
const items = (e.clipboardData || window.clipboardData).items;
for (const item of items) {
if (item.type.indexOf('image') === 0) {
const blob = item.getAsFile();
const reader = new FileReader();
reader.onload = (event) => {
imagePreview.src = event.target.result;
pastedImageDataInput.value = event.target.result;
clearOtherImageInputs('paste');
};
reader.readAsDataURL(blob);
}
}
});
pasteArea.addEventListener('dragover', (e) => { e.preventDefault(); pasteArea.classList.add('hover'); });
pasteArea.addEventListener('dragleave', () => pasteArea.classList.remove('hover'));
pasteArea.addEventListener('drop', (e) => { e.preventDefault(); pasteArea.classList.remove('hover'); });
}
if(deleteCheckbox) {
deleteCheckbox.addEventListener('change', function() {
if(this.checked) {
imagePreview.src = 'keinbild.png';
clearOtherImageInputs('delete');
} else {
imagePreview.src = originalImageSrc;
}
});
}
const consumableCheckbox = document.getElementById('consumable');
const quantityOwnedInput = document.getElementById('quantity_owned');
consumableCheckbox.addEventListener('change', function() {
if (this.checked) {
quantityOwnedInput.disabled = true;
quantityOwnedInput.value = 1;
} else {
quantityOwnedInput.disabled = false;
}
});
});
</script>
<?php require_once 'footer.php'; ?>

View File

@@ -0,0 +1,133 @@
<?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
footer.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
// footer.php - Globale Footer-Struktur
?>
<footer class="main-footer">
&copy; <?php echo date('Y'); ?> Trekking Packliste
</footer>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
const currentPath = window.location.pathname.split('/').pop();
const sidebarLinks = document.querySelectorAll('.sidebar .nav-link');
const parentMap = {
'add_article.php': 'articles.php',
'edit_article.php': 'articles.php',
'add_packing_list.php': 'packing_lists.php',
'edit_packing_list_details.php': 'packing_lists.php',
'manage_packing_list_items.php': 'packing_lists.php',
'packing_list_detail.php': 'packing_lists.php',
'share_packing_list.php': 'packing_lists.php',
'duplicate_packing_list.php': 'packing_lists.php'
};
const activePage = parentMap[currentPath] || currentPath;
sidebarLinks.forEach(link => {
const linkPath = link.getAttribute('href');
if (linkPath === activePage) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
var invitationModalElement = document.getElementById('invitationModal');
if (invitationModalElement) {
var invitationModal = new bootstrap.Modal(invitationModalElement);
invitationModal.show();
}
});
</script>
</body>
</html>

103
header.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
// header.php - Zentraler Header mit Einladungs-Check und dem KORREKTEN, vollständigen Menü
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$current_username = isset($_SESSION['username']) ? $_SESSION['username'] : 'Gast';
$pending_invitation = null;
if (isset($_SESSION['user_id'])) {
require_once 'db_connect.php';
$stmt_invite_check = $conn->prepare(
"SELECT hi.id, h.name AS household_name, u.username AS inviter_name
FROM household_invitations hi
JOIN households h ON hi.household_id = h.id
JOIN users u ON hi.inviter_user_id = u.id
WHERE hi.invited_user_id = ? AND hi.status = 'pending'"
);
if ($stmt_invite_check) {
$stmt_invite_check->bind_param("i", $_SESSION['user_id']);
$stmt_invite_check->execute();
$result = $stmt_invite_check->get_result();
if ($result->num_rows > 0) {
$pending_invitation = $result->fetch_assoc();
}
$stmt_invite_check->close();
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo isset($page_title) ? htmlspecialchars($page_title) . ' - ' : ''; ?>Trekking Packliste</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600;700&family=Poppins:wght@500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/style.css?v=<?php echo time(); ?>">
</head>
<body>
<div class="page-wrapper">
<div class="sidebar">
<a class="navbar-brand" href="index.php">
<img src="./logo.png" alt="Logo" class="navbar-brand-logo">
<h4 class="mt-2">Packliste</h4>
</a>
<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="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>
<li class="nav-item"><a class="nav-link" href="manufacturers.php"><i class="fas fa-industry fa-fw"></i>Hersteller</a></li>
<li class="nav-item"><a class="nav-link" href="household.php"><i class="fas fa-users-cog fa-fw"></i>Haushalt</a></li>
<li class="nav-item"><a class="nav-link" href="user_profile.php"><i class="fas fa-user-cog fa-fw"></i>Profil</a></li>
<li class="nav-item"><a class="nav-link" href="help.php"><i class="fas fa-question-circle fa-fw"></i>Hilfe</a></li>
</ul>
<div class="username-display">
<div>Hallo, <strong><?php echo htmlspecialchars($current_username); ?></strong></div>
<div class="mt-3">
<a class="btn btn-sm btn-outline-light w-100" href="logout.php"><i class="fas fa-sign-out-alt me-2"></i>Abmelden</a>
</div>
</div>
</div>
<div class="main-content">
<?php if ($pending_invitation): ?>
<div class="modal fade" id="invitationModal" tabindex="-1" aria-labelledby="invitationModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="invitationModalLabel"><i class="fas fa-users me-2"></i>Einladung zu einem Haushalt</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
Du wurdest von <strong><?php echo htmlspecialchars($pending_invitation['inviter_name']); ?></strong>
eingeladen, dem Haushalt "<strong><?php echo htmlspecialchars($pending_invitation['household_name']); ?></strong>" beizutreten.
</p>
<p>Möchtest du die Einladung annehmen?</p>
</div>
<div class="modal-footer">
<form action="household_actions.php" method="post" class="d-inline">
<input type="hidden" name="action" value="respond_invitation">
<input type="hidden" name="invitation_id" value="<?php echo $pending_invitation['id']; ?>">
<button type="submit" name="response" value="decline" class="btn btn-secondary">Ablehnen</button>
</form>
<form action="household_actions.php" method="post" class="d-inline">
<input type="hidden" name="action" value="respond_invitation">
<input type="hidden" name="invitation_id" value="<?php echo $pending_invitation['id']; ?>">
<button type="submit" name="response" value="accept" class="btn btn-primary">Annehmen</button>
</form>
</div>
</div>
</div>
</div>
<?php endif; ?>

188
help.php Normal file
View File

@@ -0,0 +1,188 @@
<?php
// help.php - Eine umfassende Anleitungs- und Hilfeseite für die Anwendung.
$page_title = "Hilfe & Anleitung";
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once 'header.php';
?>
<style>
.accordion-button:not(.collapsed) {
color: var(--color-primary-dark);
background-color: #eef3eb;
}
.accordion-body ul {
padding-left: 20px;
}
.accordion-body li {
margin-bottom: 10px;
}
.card-header-topic {
background-color: var(--color-primary);
color: var(--color-text-light);
}
.topic-icon {
font-size: 1.5rem;
margin-right: 15px;
}
</style>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0"><i class="fas fa-question-circle me-2"></i>Anleitung & Hilfe</h2>
</div>
<div class="card-body p-4">
<p class="lead">
Willkommen bei deiner Trekking-Packlisten-Anwendung! Diese Seite hilft dir, alle Funktionen zu verstehen und das Beste aus dem Tool herauszuholen.
</p>
<hr class="my-4">
<div class="card mb-4">
<div class="card-header card-header-topic d-flex align-items-center">
<i class="fas fa-box-open topic-icon"></i>
<h5 class="mb-0">1. Grundlagen: Dein Inventar verwalten</h5>
</div>
<div class="card-body">
<p>Das Herzstück der Anwendung sind deine <strong>Artikel</strong>. Dies ist deine persönliche Datenbank aller Ausrüstungsgegenstände. Je genauer du sie pflegst, desto präziser werden deine Packlisten.</p>
<div class="accordion" id="accordionInventory">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
<strong>Artikel anlegen und bearbeiten</strong>
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne" data-bs-parent="#accordionInventory">
<div class="accordion-body">
<ul>
<li><strong>Artikel anlegen:</strong> Gehe im Menü auf "Artikel" und klicke auf "Neuen Artikel hinzufügen". Fülle die Felder so detailliert wie möglich aus.</li>
<li><strong>Stammdaten:</strong> Gib den Namen, Hersteller und das genaue Modell an. Je spezifischer, desto besser.</li>
<li><strong>Gewicht:</strong> Das Gewicht in Gramm ist das wichtigste Feld für die spätere Analyse deiner Packliste.</li>
<li><strong>Komponenten:</strong> Ein grosser Vorteil! Du kannst Artikel als "Komponente" eines anderen Artikels definieren. Beispiel: Heringe sind eine Komponente des Zelts. So baust du logische Sets.</li>
<li><strong>Eigenschaften:</strong>
<ul>
<li><strong>Anzahl im Besitz:</strong> Wie oft besitzt du diesen Artikel? (z.B. 4x Wander-Socken).</li>
<li><strong>Verbrauchsartikel:</strong> Hake diese Box an für Dinge wie Gaskartuschen oder Müsliriegel. Sie werden anders im Gesamtgewicht berechnet.</li>
<li><strong>Für den Haushalt freigeben:</strong> Wenn du in einem Haushalt bist, kannst du diesen Artikel für alle Mitglieder sichtbar und nutzbar machen. Ideal für Zelte, Kocher etc.</li>
</ul>
</li>
<li><strong>Bild & Produktseite:</strong> Ein Bild hilft bei der visuellen Erkennung. Ein Link zur Produktseite kann nützlich sein, um Details nachzuschlagen.</li>
</ul>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<strong>Kategorien, Hersteller & Lagerorte</strong>
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#accordionInventory">
<div class="accordion-body">
Um dein Inventar übersichtlich zu halten, kannst du unter den entsprechenden Menüpunkten eigene Organisations-Strukturen anlegen:
<ul>
<li><strong>Kategorien:</strong> Definiere eigene Kategorien wie "Küche", "Schlafen", "Kleidung". Dies hilft beim Filtern und bei der Analyse der Packliste.</li>
<li><strong>Hersteller:</strong> Lege die Hersteller deiner Ausrüstung an, um sie schnell wiederzufinden.</li>
<li><strong>Lagerorte:</strong> Wo bewahrst du deine Ausrüstung auf? Definiere hier eine zweistufige Hierarchie (z.B. Ebene 1: "Keller", Ebene 2: "Regal 3, Box B"), um nie wieder etwas suchen zu müssen.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header card-header-topic d-flex align-items-center">
<i class="fas fa-clipboard-list topic-icon"></i>
<h5 class="mb-0">2. Packlisten erstellen und nutzen</h5>
</div>
<div class="card-body">
<p>Sobald dein Inventar gepflegt ist, kannst du mit dem Packen beginnen.</p>
<div class="accordion" id="accordionPacking">
<div class="accordion-item">
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<strong>Die interaktive Pack-Ansicht (manage_packing_list_items.php)</strong>
</button>
</h2>
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-bs-parent="#accordionPacking">
<div class="accordion-body">
Dies ist die zentrale Seite zur Zusammenstellung deiner Liste.
<ul>
<li><strong>Artikel hinzufügen:</strong> Ziehe einfach einen Artikel aus der linken Spalte ("Verfügbare Artikel") in eine der Träger-Boxen auf der rechten Seite.</li>
<li><strong>Position ändern:</strong> Du kannst die Artikel jederzeit per Drag-and-Drop neu anordnen, sowohl innerhalb einer Träger-Box als auch zwischen verschiedenen Trägern.</li>
<li><strong>Ebenen erstellen (Verschachteln):</strong> Klicke auf den <strong>Pfeil nach rechts</strong> bei einem Artikel, um ihn eine Ebene tiefer zu setzen. So wird er zum Kind-Element des Artikels direkt darüber. Das ist perfekt für Sets (z.B. "Kochset" mit Topf, Löffel, Brenner als Kinder). Mit dem <strong>Pfeil nach links</strong> hebst du die Verschachtelung wieder auf. Du kannst mehrere Ebenen tief verschachteln.</li>
<li><strong>Menge anpassen:</strong> Ändere die Zahl im Eingabefeld, um die Menge eines Artikels für diese spezifische Packliste anzupassen.</li>
<li><strong>Komponenten-Abfrage:</strong> Wenn du einen Artikel hinzufügst, der in deinem Inventar als Set mit Komponenten definiert ist, fragt dich das System, ob du die zugehörigen Komponenten ebenfalls hinzufügen möchtest.</li>
<li><strong>Träger (Carriers):</strong> Standardmässig bist du der einzige Träger. In einem Haushalt erscheinen hier alle Mitglieder. Du kannst die Ausrüstung per Drag-and-Drop auf verschiedene Personen verteilen, um das Gewicht fair aufzuteilen.</li>
</ul>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingFour">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
<strong>Packliste ansehen und analysieren</strong>
</button>
</h2>
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour" data-bs-parent="#accordionPacking">
<div class="accordion-body">
Die Detailansicht einer Packliste gibt dir eine perfekte Übersicht:
<ul>
<li><strong>Statistiken:</strong> Auf der rechten Seite siehst du das Gesamtgewicht, aufgeteilt nach Trägern und Kategorien. So erkennst du sofort, wo das meiste Gewicht herkommt.</li>
<li><strong>Basisgewicht:</strong> Das System berechnet automatisch dein Basisgewicht (ohne Verbrauchsartikel), eine wichtige Kennzahl für jeden Wanderer.</li>
<li><strong>Druckansicht:</strong> Über den "Drucken"-Button kannst du eine saubere, minimalistische Checkliste für deine Tour ausdrucken.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header card-header-topic d-flex align-items-center">
<i class="fas fa-users topic-icon"></i>
<h5 class="mb-0">3. Zusammenarbeit im Haushalt & Teilen</h5>
</div>
<div class="card-body">
<p>Teile deine Ausrüstung und Pläne mit anderen.</p>
<div class="accordion" id="accordionSharing">
<div class="accordion-item">
<h2 class="accordion-header" id="headingFive">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFive" aria-expanded="false" aria-controls="collapseFive">
<strong>Das Haushalts-System</strong>
</button>
</h2>
<div id="collapseFive" class="accordion-collapse collapse" aria-labelledby="headingFive" data-bs-parent="#accordionSharing">
<div class="accordion-body">
<ul>
<li><strong>Erstellen & Beitreten:</strong> Unter "Haushalt" kannst du einen neuen Haushalt gründen oder eine Einladung annehmen. Du kannst nur in einem Haushalt gleichzeitig sein.</li>
<li><strong>Geteilte Ressourcen:</strong> Artikel und Packlisten, die für den Haushalt freigegeben sind, können von allen Mitgliedern gesehen und verwendet werden.</li>
<li><strong>Admin-Rolle:</strong> Der Gründer eines Haushalts ist der Administrator und kann neue Mitglieder per Benutzername einladen.</li>
<li><strong>Aktivitäten-Log:</strong> Auf der Haushalts-Seite siehst du einen Verlauf der letzten Aktivitäten, z.B. wer einen neuen Artikel hinzugefügt oder eine Liste bearbeitet hat.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<?php require_once 'footer.php'; ?>

158
household.php Normal file
View File

@@ -0,0 +1,158 @@
<?php
// household.php - Zentrale Seite für die Haushaltsübersicht und -verwaltung
$page_title = "Mein Haushalt";
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'];
$message = '';
if (isset($_SESSION['message'])) {
$message = '<div class="alert alert-'.$_SESSION['message_type'].'" role="alert">'.htmlspecialchars($_SESSION['message']).'</div>';
unset($_SESSION['message']);
unset($_SESSION['message_type']);
}
// Lade Haushalts-Informationen
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$household_id = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
$household_data = null;
$household_members_stats = [];
$household_logs = [];
$is_admin = false;
if ($household_id) {
// Haushaltsdetails laden
$stmt_details = $conn->prepare("SELECT * FROM households WHERE id = ?");
$stmt_details->bind_param("i", $household_id);
$stmt_details->execute();
$household_data = $stmt_details->get_result()->fetch_assoc();
$is_admin = ($household_data && $household_data['admin_user_id'] == $current_user_id);
$stmt_details->close();
// Mitglieder und deren Statistiken laden
$stmt_members = $conn->prepare("
SELECT
u.id, u.username,
(SELECT COUNT(*) FROM articles WHERE user_id = u.id) as article_count,
(SELECT COUNT(*) FROM packing_lists WHERE user_id = u.id) as list_count
FROM users u
WHERE u.household_id = ?
ORDER BY u.username
");
$stmt_members->bind_param("i", $household_id);
$stmt_members->execute();
$household_members_stats = $stmt_members->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_members->close();
// Eingeladene Mitglieder laden
$stmt_pending = $conn->prepare("SELECT u.username FROM household_invitations hi JOIN users u ON hi.invited_user_id = u.id WHERE hi.household_id = ? AND hi.status = 'pending'");
$stmt_pending->bind_param("i", $household_id);
$stmt_pending->execute();
$pending_invitations = $stmt_pending->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_pending->close();
// Haushalts-Log laden
$stmt_logs = $conn->prepare("SELECT message, created_at FROM household_logs WHERE household_id = ? ORDER BY created_at DESC LIMIT 20");
$stmt_logs->bind_param("i", $household_id);
$stmt_logs->execute();
$household_logs = $stmt_logs->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_logs->close();
}
$conn->close();
?>
<div class="row g-4">
<div class="col-lg-5">
<div class="card">
<div class="card-header"><h2 class="h4 mb-0"><i class="fas fa-home me-2"></i>Mein Haushalt</h2></div>
<div class="card-body p-4">
<?php if(!empty($message)) echo $message; ?>
<?php if (!$household_id): ?>
<p>Du bist derzeit in keinem Haushalt. Erstelle einen neuen Haushalt, um Packlisten und Artikel mit anderen zu teilen.</p>
<form action="household_actions.php" method="post">
<input type="hidden" name="action" value="create_household">
<div class="row g-3 align-items-end">
<div class="col-sm-8"><label for="household_name" class="form-label">Name deines Haushalts</label><input type="text" class="form-control" name="household_name" id="household_name" placeholder="z.B. Familie Mustermann" required></div>
<div class="col-sm-4"><button type="submit" class="btn btn-primary w-100">Erstellen</button></div>
</div>
</form>
<?php else: ?>
<h4><?php echo htmlspecialchars($household_data['name']); ?></h4>
<?php if ($is_admin) echo '<span class="badge bg-success mb-3">Du bist Administrator dieses Haushalts</span>'; ?>
<h5 class="mt-4">Mitglieder</h5>
<ul class="list-group">
<?php foreach ($household_members_stats as $member): ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><i class="fas fa-user me-2"></i><?php echo htmlspecialchars($member['username']); ?></span>
<div>
<span class="badge bg-secondary rounded-pill" data-bs-toggle="tooltip" title="Erstellte Artikel"><i class="fas fa-box fa-xs me-1"></i> <?php echo $member['article_count']; ?></span>
<span class="badge bg-secondary rounded-pill" data-bs-toggle="tooltip" title="Erstellte Packlisten"><i class="fas fa-clipboard-list fa-xs me-1"></i> <?php echo $member['list_count']; ?></span>
</div>
</li>
<?php endforeach; ?>
<?php foreach ($pending_invitations as $pending): ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><i class="fas fa-user-clock me-2 text-muted"></i><?php echo htmlspecialchars($pending['username']); ?></span>
<span class="badge bg-warning text-dark rounded-pill">Eingeladen</span>
</li>
<?php endforeach; ?>
</ul>
<?php if ($is_admin): ?>
<hr class="my-4">
<h5>Neues Mitglied einladen</h5>
<form action="household_actions.php" method="post">
<input type="hidden" name="action" value="invite_member">
<div class="row g-3 align-items-end">
<div class="col-sm-8"><label for="username" class="form-label">Benutzername des Mitglieds</label><input type="text" class="form-control" id="username" name="username" required></div>
<div class="col-sm-4"><button type="submit" class="btn btn-primary w-100"><i class="fas fa-paper-plane me-2"></i>Einladen</button></div>
</div>
</form>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="card h-100">
<div class="card-header"><h2 class="h4 mb-0"><i class="fas fa-stream me-2"></i>Aktivitäten im Haushalt</h2></div>
<div class="card-body p-2">
<?php if (empty($household_logs)): ?>
<div class="p-4 text-center text-muted">Noch keine Aktivitäten vorhanden.</div>
<?php else: ?>
<ul class="list-group list-group-flush">
<?php foreach ($household_logs as $log): ?>
<li class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<p class="mb-1"><?php echo htmlspecialchars($log['message']); ?></p>
<small class="text-muted"><?php echo date('d.m.Y H:i', strtotime($log['created_at'])); ?></small>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php require_once 'footer.php'; ?>

143
household_actions.php Normal file
View File

@@ -0,0 +1,143 @@
<?php
// household_actions.php - Serverseitige Logik für alle Haushalts-Aktionen
// KORREKTUR: Leitet jetzt zur household.php weiter
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once 'db_connect.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
function log_household_action($conn, $household_id, $user_id, $message) {
if (empty($household_id)) return;
$stmt = $conn->prepare("INSERT INTO household_logs (household_id, user_id, message) VALUES (?, ?, ?)");
$stmt->bind_param("iis", $household_id, $user_id, $message);
$stmt->execute();
$stmt->close();
}
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['action'])) {
$action = $_POST['action'];
$current_username = $_SESSION['username'];
$conn->begin_transaction();
try {
switch ($action) {
case 'create_household':
$household_name = trim($_POST['household_name']);
if (empty($household_name)) throw new Exception("Der Haushaltsname darf nicht leer sein.");
$stmt_check = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_check->bind_param("i", $current_user_id);
$stmt_check->execute();
$user_data = $stmt_check->get_result()->fetch_assoc();
if (!empty($user_data['household_id'])) throw new Exception("Du bist bereits Mitglied in einem Haushalt.");
$stmt_check->close();
$stmt_create = $conn->prepare("INSERT INTO households (name, admin_user_id) VALUES (?, ?)");
$stmt_create->bind_param("si", $household_name, $current_user_id);
$stmt_create->execute();
$new_household_id = $conn->insert_id;
$stmt_create->close();
$stmt_update_user = $conn->prepare("UPDATE users SET household_id = ? WHERE id = ?");
$stmt_update_user->bind_param("ii", $new_household_id, $current_user_id);
$stmt_update_user->execute();
$stmt_update_user->close();
$log_message = htmlspecialchars($current_username) . " hat den Haushalt '" . htmlspecialchars($household_name) . "' erstellt.";
log_household_action($conn, $new_household_id, $current_user_id, $log_message);
$message = "Haushalt erfolgreich erstellt!";
$_SESSION['message_type'] = 'success';
break;
case 'invite_member':
$invited_username = trim($_POST['username']);
if (empty($invited_username)) throw new Exception("Der Benutzername darf nicht leer sein.");
$stmt_find = $conn->prepare("SELECT id, household_id FROM users WHERE username = ?");
$stmt_find->bind_param("s", $invited_username);
$stmt_find->execute();
$invited_user = $stmt_find->get_result()->fetch_assoc();
$stmt_find->close();
if (!$invited_user) throw new Exception("Benutzer '" . htmlspecialchars($invited_username) . "' nicht gefunden.");
if ($invited_user['id'] == $current_user_id) throw new Exception("Du kannst dich nicht selbst einladen.");
if (!empty($invited_user['household_id'])) throw new Exception("Dieser Benutzer ist bereits in einem anderen Haushalt.");
$stmt_household = $conn->prepare("SELECT id FROM households WHERE admin_user_id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$household = $stmt_household->get_result()->fetch_assoc();
if (!$household) throw new Exception("Nur der Admin kann Mitglieder einladen.");
$stmt_household->close();
$stmt_invite = $conn->prepare("INSERT INTO household_invitations (household_id, inviter_user_id, invited_user_id) VALUES (?, ?, ?)");
$stmt_invite->bind_param("iii", $household['id'], $current_user_id, $invited_user['id']);
$stmt_invite->execute();
$stmt_invite->close();
$log_message = htmlspecialchars($current_username) . " hat " . htmlspecialchars($invited_username) . " in den Haushalt eingeladen.";
log_household_action($conn, $household['id'], $current_user_id, $log_message);
$message = "Einladung an " . htmlspecialchars($invited_username) . " wurde versendet.";
$_SESSION['message_type'] = 'success';
break;
case 'respond_invitation':
$invitation_id = intval($_POST['invitation_id']);
$response = $_POST['response'];
if (!in_array($response, ['accept', 'decline'])) throw new Exception("Ungültige Antwort.");
$stmt_get_invite = $conn->prepare("SELECT id, household_id, status FROM household_invitations WHERE id = ? AND invited_user_id = ?");
$stmt_get_invite->bind_param("ii", $invitation_id, $current_user_id);
$stmt_get_invite->execute();
$invite = $stmt_get_invite->get_result()->fetch_assoc();
$stmt_get_invite->close();
if (!$invite || $invite['status'] !== 'pending') throw new Exception("Einladung ist ungültig oder wurde bereits beantwortet.");
if ($response === 'accept') {
$stmt_accept = $conn->prepare("UPDATE users SET household_id = ? WHERE id = ?");
$stmt_accept->bind_param("ii", $invite['household_id'], $current_user_id);
$stmt_accept->execute();
$stmt_accept->close();
$stmt_update_invite = $conn->prepare("UPDATE household_invitations SET status = 'accepted', responded_at = NOW() WHERE id = ?");
$stmt_update_invite->bind_param("i", $invitation_id);
$stmt_update_invite->execute();
$stmt_update_invite->close();
$log_message = htmlspecialchars($current_username) . " ist dem Haushalt beigetreten.";
log_household_action($conn, $invite['household_id'], $current_user_id, $log_message);
$message = "Du bist dem Haushalt beigetreten!";
$_SESSION['message_type'] = 'success';
} else {
$stmt_update_invite = $conn->prepare("UPDATE household_invitations SET status = 'declined', responded_at = NOW() WHERE id = ?");
$stmt_update_invite->bind_param("i", $invitation_id);
$stmt_update_invite->execute();
$stmt_update_invite->close();
$message = "Du hast die Einladung abgelehnt.";
$_SESSION['message_type'] = 'info';
}
break;
}
$conn->commit();
} catch (Exception $e) {
$conn->rollback();
$message = $e->getMessage();
$_SESSION['message_type'] = 'danger';
}
$_SESSION['message'] = $message;
header("Location: household.php");
exit;
}
?>

223
index.php Normal file
View File

@@ -0,0 +1,223 @@
<?php
// index.php - Startseite der Trekking Packlisten Anwendung
// KORREKTUR: Farbpalette für die Diagramme für bessere Lesbarkeit überarbeitet.
$page_title = "Dashboard";
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'];
$current_username = $_SESSION['username'];
// --- Lade Haushalts-Informationen ---
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$current_user_household_id = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
$household_member_ids = [$current_user_id];
if ($current_user_household_id) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $current_user_household_id);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) {
if (!in_array($row['id'], $household_member_ids)) {
$household_member_ids[] = $row['id'];
}
}
$stmt_members->close();
}
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
// --- Daten für die Statistik-Boxen laden ---
$articles_by_category = [];
$sql_articles_by_category = "SELECT c.name AS category_name, COUNT(a.id) AS article_count FROM articles a LEFT JOIN categories c ON a.category_id = c.id WHERE a.user_id IN ($placeholders) OR a.household_id = ? GROUP BY c.name ORDER BY article_count DESC";
$stmt_articles_by_category = $conn->prepare($sql_articles_by_category);
if ($stmt_articles_by_category) {
$all_params = array_merge($household_member_ids, [$current_user_household_id]);
$all_types = $types . 'i';
$stmt_articles_by_category->bind_param($all_types, ...$all_params);
$stmt_articles_by_category->execute();
$result_articles_by_category = $stmt_articles_by_category->get_result();
while ($row = $result_articles_by_category->fetch_assoc()) {
$articles_by_category[] = [
'category_name' => $row['category_name'] ?: 'Ohne Kategorie',
'article_count' => $row['article_count']
];
}
$stmt_articles_by_category->close();
}
$packing_lists_stats = [];
$sql_lists = "SELECT pl.id, pl.name, SUM(pli.quantity * a.weight_grams) AS total_weight FROM packing_lists pl LEFT JOIN packing_list_items pli ON pl.id = pli.packing_list_id LEFT JOIN articles a ON pli.article_id = a.id WHERE pl.user_id IN ($placeholders) OR pl.household_id = ? GROUP BY pl.id, pl.name ORDER BY total_weight DESC";
$stmt_lists = $conn->prepare($sql_lists);
if ($stmt_lists) {
$all_params = array_merge($household_member_ids, [$current_user_household_id]);
$all_types = $types . 'i';
$stmt_lists->bind_param($all_types, ...$all_params);
$stmt_lists->execute();
$result_lists = $stmt_lists->get_result();
while ($row = $result_lists->fetch_assoc()) {
$packing_lists_stats[] = [
'packing_list_id' => $row['id'],
'packing_list_name' => htmlspecialchars($row['name']),
'total_weight_grams' => $row['total_weight'] ?: 0
];
}
$stmt_lists->close();
}
$conn->close();
// Daten für die Diagramme vorbereiten
$category_chart_labels = json_encode(array_column($articles_by_category, 'category_name'));
$category_chart_data = json_encode(array_column($articles_by_category, 'article_count'));
$list_chart_labels = json_encode(array_column($packing_lists_stats, 'packing_list_name'));
$list_chart_data = json_encode(array_column($packing_lists_stats, 'total_weight_grams'));
$quotes = ["Nur wo du zu Fuß warst, bist du auch wirklich gewesen.", "Der Weg ist das Ziel.", "Abenteuer beginnen, wo Pläne enden."];
$random_quote = $quotes[array_rand($quotes)];
?>
<div class="card p-4 border-0 shadow-sm">
<div class="welcome-section">
<h1>Willkommen zurück, <strong><?php echo htmlspecialchars($current_username); ?></strong>!</h1>
<p class="lead">Organisiere deine Ausrüstung und bereite dich optimal auf dein nächstes Abenteuer vor.</p>
<div class="welcome-image-container">
<div class="animated-bg"></div>
</div>
<p class="text-muted fst-italic mt-4">"<?php echo $random_quote; ?>"</p>
</div>
<hr class="my-5" style="opacity: 0.1;">
<div class="row g-4">
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header"><h3 class="h5 mb-0"><i class="fas fa-chart-pie me-2"></i>Artikelverteilung im Haushalt</h3></div>
<div class="card-body d-flex align-items-center justify-content-center">
<?php if (empty($articles_by_category)): ?>
<p class="text-center text-muted p-4">Füge Artikel hinzu, um hier eine Übersicht zu sehen.</p>
<?php else: ?>
<div class="chart-container w-100">
<canvas id="categoryChart"></canvas>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header"><h3 class="h5 mb-0"><i class="fas fa-chart-bar me-2"></i>Gewicht pro Packliste im Haushalt (g)</h3></div>
<div class="card-body d-flex align-items-center justify-content-center">
<?php if (empty($packing_lists_stats)): ?>
<p class="text-center text-muted p-4">Erstelle eine Packliste, um hier eine Analyse zu sehen.</p>
<?php else: ?>
<div class="chart-container w-100">
<canvas id="listWeightChart"></canvas>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// KORREKTUR: Neue, kontrastreichere Farbpalette
const chartColors = [
'#4CAF50', '#2196F3', '#FFC107', '#E91E63', '#9C27B0',
'#00BCD4', '#FF5722', '#795548', '#607D8B', '#F44336'
];
const categoryCtx = document.getElementById('categoryChart');
if (categoryCtx && <?php echo $category_chart_data; ?>.length > 0) {
new Chart(categoryCtx, {
type: 'doughnut',
data: {
labels: <?php echo $category_chart_labels; ?>,
datasets: [{
label: 'Anzahl Artikel',
data: <?php echo $category_chart_data; ?>,
backgroundColor: chartColors,
hoverOffset: 4,
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
usePointStyle: true,
padding: 20
}
}
},
cutout: '70%'
}
});
}
const listWeightCtx = document.getElementById('listWeightChart');
if (listWeightCtx && <?php echo $list_chart_data; ?>.length > 0) {
new Chart(listWeightCtx, {
type: 'bar',
data: {
labels: <?php echo $list_chart_labels; ?>,
datasets: [{
label: 'Gesamtgewicht in Gramm',
data: <?php echo $list_chart_data; ?>,
backgroundColor: 'rgba(107, 142, 35, 0.8)', // Using primary light color
borderColor: 'rgba(59, 74, 35, 1)',
borderWidth: 1,
borderRadius: 4
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
beginAtZero: true,
grid: {
display: false
}
},
y: {
grid: {
color: 'rgba(0,0,0,0.05)'
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
});
</script>
<?php require_once 'footer.php'; ?>

BIN
keinbild.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

101
login.php Normal file
View File

@@ -0,0 +1,101 @@
<?php
// login.php - Seite zur Benutzeranmeldung
// KORREKTUR: Design an das der Registrierungsseite angepasst.
$page_title = "Anmeldung";
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (isset($_SESSION['user_id'])) {
header("Location: index.php");
exit;
}
require_once 'db_connect.php';
$message = '';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = trim($_POST['username']);
$password = trim($_POST['password']);
if (empty($username) || empty($password)) {
$message = '<div class="alert alert-danger" role="alert">Bitte füllen Sie beide Felder aus.</div>';
} else {
$stmt = $conn->prepare("SELECT id, username, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows == 1) {
$user = $result->fetch_assoc();
if (password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$stmt_settings = $conn->prepare("SELECT setting_key, setting_value FROM user_settings WHERE user_id = ?");
$stmt_settings->bind_param("i", $user['id']);
$stmt_settings->execute();
$result_settings = $stmt_settings->get_result();
while ($setting = $result_settings->fetch_assoc()) {
$_SESSION[$setting['setting_key']] = $setting['setting_value'];
}
$stmt_settings->close();
header("Location: index.php");
exit;
} else {
$message = '<div class="alert alert-danger" role="alert">Ungültiger Benutzername oder Passwort.</div>';
}
} else {
$message = '<div class="alert alert-danger" role="alert">Ungültiger Benutzername oder Passwort.</div>';
}
$stmt->close();
}
}
$conn->close();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anmeldung - Trekking Packliste</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<div class="auth-wrapper">
<div class="auth-container">
<div class="auth-header">
<img src="./logo.png" alt="Trekking Packliste Logo" class="auth-logo">
<h2>Anmeldung</h2>
</div>
<div class="auth-body">
<?php if(!empty($message)) echo $message; ?>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100 mt-3">Anmelden</button>
</form>
<div class="text-center mt-4">
<span class="text-muted">Noch nicht registriert?</span> <a href="register.php" class="fw-bold text-decoration-none" style="color: var(--color-primary);">Jetzt registrieren</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
</body>
</html>

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

29
logout.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
// logout.php - Skript zur Abmeldung
// Session starten
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Alle Session-Variablen löschen
$_SESSION = array();
// Wenn die Session über Cookies verwaltet wird, lösche auch den Session-Cookie
// Beachte: Dies löscht nicht die Session-Datei auf dem Server
// sondern nur den Cookie auf der Clientseite.
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// Die Session zerstören
session_destroy();
// Zur Login-Seite umleiten
header("Location: login.php");
exit;
?>

View File

@@ -0,0 +1,492 @@
<?php
// manage_packing_list_items.php - Interaktive Verwaltung der Packlisten-Artikel
// FINALE, REKURSIVE NESTED-SORTABLE VERSION
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;
$can_edit = false;
// Lade Packliste und prüfe Berechtigung
$stmt_list = $conn->prepare("SELECT * FROM packing_lists WHERE id = ?");
$stmt_list->bind_param("i", $packing_list_id);
$stmt_list->execute();
$result = $stmt_list->get_result();
if ($result->num_rows === 0) die("Packliste nicht gefunden.");
$packing_list = $result->fetch_assoc();
$stmt_list->close();
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$current_user_household_id = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
$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; }
if (!$can_edit) { die("Zugriff verweigert."); }
$page_title = "Packliste bearbeiten: " . htmlspecialchars($packing_list['name']);
$household_member_ids = [$current_user_id];
if ($current_user_household_id) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $current_user_household_id);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) { if (!in_array($row['id'], $household_member_ids)) { $household_member_ids[] = $row['id']; } }
$stmt_members->close();
}
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
$sql_all_articles = "SELECT a.id, a.name, a.weight_grams, a.quantity_owned, a.product_designation, a.consumable, a.parent_article_id, c.name as category_name, m.name as manufacturer_name
FROM articles a
LEFT JOIN categories c ON a.category_id = c.id
LEFT JOIN manufacturers m ON a.manufacturer_id = m.id
WHERE a.user_id IN ($placeholders)";
$all_params = $household_member_ids;
$all_types = $types;
if ($current_user_household_id) {
$sql_all_articles .= " OR a.household_id = ?";
$all_params[] = $current_user_household_id;
$all_types .= 'i';
}
$stmt_all_articles = $conn->prepare($sql_all_articles);
$stmt_all_articles->bind_param($all_types, ...$all_params);
$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) : [];
$carriers_data = [];
$stmt_carriers = $conn->prepare("SELECT id, username FROM users WHERE id IN ($placeholders)");
$stmt_carriers->bind_param($types, ...$household_member_ids);
$stmt_carriers->execute();
$carriers_result = $stmt_carriers->get_result();
while ($row = $carriers_result->fetch_assoc()) { $carriers_data[] = $row; }
$stmt_carriers->close();
$carriers_data[] = ['id' => null, 'username' => 'Sonstiges'];
$categories = array_unique(array_filter(array_column($all_articles_raw, 'category_name')));
sort($categories);
$manufacturers = array_unique(array_filter(array_column($all_articles_raw, 'manufacturer_name')));
sort($manufacturers);
$conn->close();
?>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0"><i class="fas fa-edit me-2"></i>Packliste bearbeiten: <?php echo htmlspecialchars($packing_list['name']); ?></h2>
<a href="packing_list_detail.php?id=<?php echo $packing_list_id; ?>" class="btn btn-secondary"><i class="fas fa-eye me-2"></i>Zur Ansicht</a>
</div>
<div class="card-body">
<div class="editor-container">
<div class="editor-pane">
<div class="pane-header">
<h5 class="mb-0">Verfügbare Artikel</h5>
<div class="row g-2 mt-2">
<div class="col-12"><input type="text" id="filter-text" class="form-control form-control-sm" placeholder="Artikel, Hersteller, Modell suchen..."></div>
<div class="col-md-6"><select id="filter-category" class="form-select form-select-sm"><option value="">Alle Kategorien</option><?php foreach($categories as $c) echo '<option>'.htmlspecialchars($c).'</option>'; ?></select></div>
<div class="col-md-6"><select id="filter-manufacturer" class="form-select form-select-sm"><option value="">Alle Hersteller</option><?php foreach($manufacturers as $m) echo '<option>'.htmlspecialchars($m).'</option>'; ?></select></div>
</div>
</div>
<div class="pane-content" id="available-items-list"></div>
</div>
<div class="editor-pane">
<div class="pane-header"><h5 class="mb-0">Gepackte Artikel</h5></div>
<div class="pane-content" id="carriers-container"></div>
</div>
</div>
</div>
</div>
<!-- Modals and Feedback remain unchanged -->
<div class="modal fade" id="includeChildrenModal" tabindex="-1" aria-labelledby="includeChildrenModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="includeChildrenModalLabel">Komponenten hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Dieser Artikel hat zugehörige Komponenten. Sollen diese ebenfalls zur Packliste hinzugefügt werden?
<ul id="children-list" class="list-group mt-2"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="add-without-children">Nein, nur diesen Artikel</button>
<button type="button" class="btn btn-primary" id="add-with-children">Ja, alle hinzufügen</button>
</div>
</div>
</div>
</div>
<div id="save-feedback" class="save-feedback">Änderungen gespeichert!</div>
<style>
/* Nested Sortable Specific Styles */
.nested-sortable {
min-height: 10px;
padding-left: 20px; /* Indent child items */
border-left: 2px solid rgba(0,0,0,0.05);
margin-top: 5px;
}
.packed-item-container {
margin-bottom: 5px;
}
.packed-item-content {
display: flex;
align-items: center;
padding: 8px;
background-color: #fff;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 6px;
transition: box-shadow 0.2s;
}
.packed-item-content:hover {
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
/* Highlight drop targets */
.nested-sortable.sortable-ghost {
background-color: rgba(59, 74, 35, 0.1);
border: 1px dashed var(--color-primary);
}
/* Empty nested containers should have a hint of height to be droppable */
.nested-sortable:empty {
min-height: 30px;
background-color: rgba(0,0,0,0.02);
border: 1px dashed rgba(0,0,0,0.1);
border-radius: 4px;
margin-top: 5px;
}
.nested-sortable:empty::after {
content: 'Hier ablegen';
display: block;
text-align: center;
color: #aaa;
font-size: 0.8em;
padding-top: 5px;
}
</style>
<script>
let packedItems = <?php echo json_encode($packed_items_raw); ?>;
const allArticles = <?php echo json_encode($all_articles_raw); ?>;
const carriers = <?php echo json_encode($carriers_data); ?>;
const packingListId = <?php echo $packing_list_id; ?>;
let sortableInstances = [];
let childrenModal = null;
document.addEventListener('DOMContentLoaded', () => {
['input', 'change'].forEach(evt => {
document.getElementById('filter-text').addEventListener(evt, renderAvailableItems);
document.getElementById('filter-category').addEventListener(evt, renderAvailableItems);
document.getElementById('filter-manufacturer').addEventListener(evt, renderAvailableItems);
});
document.getElementById('carriers-container').addEventListener('click', handlePackedItemActions);
document.getElementById('carriers-container').addEventListener('change', handleQuantityChange);
fullRender();
});
function fullRender() {
renderAvailableItems();
renderCarriersAndPackedItems();
}
function renderAvailableItems() {
const filterText = document.getElementById('filter-text').value.toLowerCase();
const filterCategory = document.getElementById('filter-category').value;
const filterManufacturer = document.getElementById('filter-manufacturer').value;
const availableListEl = document.getElementById('available-items-list');
const packedArticleIds = packedItems.map(item => String(item.article_id));
let html = '';
allArticles.forEach(article => {
if (article.parent_article_id) return;
if (article.consumable == 1 || !packedArticleIds.includes(String(article.id))) {
const matchesText = article.name.toLowerCase().includes(filterText) || (article.manufacturer_name && article.manufacturer_name.toLowerCase().includes(filterText)) || (article.product_designation && article.product_designation.toLowerCase().includes(filterText));
const matchesCategory = !filterCategory || article.category_name === filterCategory;
const matchesManufacturer = !filterManufacturer || article.manufacturer_name === filterManufacturer;
if (matchesText && matchesCategory && matchesManufacturer) {
const details = [article.manufacturer_name, article.product_designation].filter(Boolean).join(' - ');
html += `<div class="available-item-card" data-article-id="${article.id}">${article.name} <small class="text-muted">(${details || '---'} | ${article.weight_grams}g)</small></div>`;
}
}
});
availableListEl.innerHTML = html;
new Sortable(availableListEl, {
group: { name: 'nested', pull: 'clone', put: false },
animation: 150,
sort: false
});
}
function renderCarriersAndPackedItems() {
const container = document.getElementById('carriers-container');
container.innerHTML = '';
sortableInstances.forEach(s => s.destroy());
sortableInstances = [];
carriers.forEach(carrier => {
const carrierId = carrier.id === null ? 'null' : carrier.id;
const carrierDiv = document.createElement('div');
carrierDiv.className = 'carrier-box';
carrierDiv.innerHTML = `<div class="carrier-header"><h6>${carrier.username}</h6></div>`;
const carrierRootList = document.createElement('div');
carrierRootList.className = 'carrier-list nested-sortable';
carrierRootList.dataset.carrierId = carrierId;
carrierDiv.appendChild(carrierRootList);
container.appendChild(carrierDiv);
const itemsForCarrier = packedItems.filter(item => (item.carrier_user_id === null ? 'null' : String(item.carrier_user_id)) == String(carrierId));
const itemsById = itemsForCarrier.reduce((acc, item) => ({...acc, [item.id]: {...item, children: []}}), {});
const rootItems = [];
Object.values(itemsById).forEach(item => {
if (item.parent_packing_list_item_id && itemsById[item.parent_packing_list_item_id]) {
itemsById[item.parent_packing_list_item_id].children.push(item);
} else {
rootItems.push(item);
}
});
renderRecursive(rootItems, carrierRootList);
initNestedSortable(carrierRootList);
});
}
function renderRecursive(items, container) {
items.forEach(item => {
const itemEl = createPackedItemDOM(item);
container.appendChild(itemEl);
const nestedContainer = itemEl.querySelector('.nested-sortable');
if (item.children && item.children.length > 0) {
renderRecursive(item.children, nestedContainer);
}
initNestedSortable(nestedContainer);
});
}
function createPackedItemDOM(item) {
const div = document.createElement('div');
div.className = 'packed-item-container';
div.dataset.itemId = item.id;
div.dataset.articleId = item.article_id;
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="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>
</div>
</div>
<div class="nested-sortable"></div>
`;
return div;
}
function initNestedSortable(element) {
const s = new Sortable(element, {
group: 'nested',
animation: 150,
handle: '.handle',
fallbackOnBody: true,
swapThreshold: 0.65,
ghostClass: 'sortable-ghost',
onAdd: function (evt) {
if (evt.from.id === 'available-items-list') {
const articleId = evt.item.dataset.articleId;
const originalArticleData = allArticles.find(a => String(a.id) === articleId);
const newItemData = {
id: 'new-' + Date.now(),
article_id: articleId,
name: originalArticleData.name,
manufacturer_name: originalArticleData.manufacturer_name,
quantity: 1,
consumable: originalArticleData.consumable,
children: []
};
const newDOM = createPackedItemDOM(newItemData);
evt.item.replaceWith(newDOM);
initNestedSortable(newDOM.querySelector('.nested-sortable'));
const childArticles = allArticles.filter(a => String(a.parent_article_id) === articleId);
if (childArticles.length > 0) {
handleChildArticlesPrompt(childArticles, newDOM);
} else {
syncListState();
}
} else {
syncListState();
}
},
onEnd: function (evt) {
if (evt.from.id !== 'available-items-list') {
syncListState();
}
}
});
sortableInstances.push(s);
}
function handleChildArticlesPrompt(childArticles, parentElement) {
if (!childrenModal) childrenModal = new bootstrap.Modal(document.getElementById('includeChildrenModal'));
document.getElementById('children-list').innerHTML = childArticles.map(c => `<li class="list-group-item">${c.name}</li>`).join('');
document.getElementById('add-with-children').onclick = () => {
childrenModal.hide();
const nestedContainer = parentElement.querySelector('.nested-sortable');
childArticles.forEach(child => {
const childItemData = {
id: 'new-child-' + Date.now() + Math.random(),
article_id: child.id,
name: child.name,
manufacturer_name: child.manufacturer_name,
quantity: 1,
consumable: child.consumable,
children: []
};
const childDOM = createPackedItemDOM(childItemData);
nestedContainer.appendChild(childDOM);
initNestedSortable(childDOM.querySelector('.nested-sortable'));
});
syncListState();
};
document.getElementById('add-without-children').onclick = () => {
childrenModal.hide();
syncListState();
};
childrenModal.show();
}
function syncListState() {
const payload = { action: 'sync_list', list: [] };
document.querySelectorAll('.carrier-list').forEach(rootList => {
const carrierId = rootList.dataset.carrierId;
function traverse(container, parentId) {
Array.from(container.children).forEach(child => {
if (!child.classList.contains('packed-item-container')) return;
const pliId = child.dataset.itemId;
const articleId = child.dataset.articleId;
payload.list.push({
pli_id: pliId,
article_id: articleId,
carrier_id: carrierId,
parent_pli_id: parentId
});
const nestedContainer = child.querySelector('.nested-sortable');
if (nestedContainer) {
traverse(nestedContainer, pliId);
}
});
}
traverse(rootList, null);
});
sendApiRequest(payload).then(newItems => {
packedItems = newItems;
// Save scroll positions
const scrollPos = {
avail: document.getElementById('available-items-list').scrollTop,
carrier: document.getElementById('carriers-container').scrollTop // Note: carriers-container itself might not scroll, but the panes do
};
// The actual scrolling element is .pane-content
const panes = document.querySelectorAll('.pane-content');
const scrollMap = Array.from(panes).map(p => p.scrollTop);
fullRender();
// Restore scroll positions
Array.from(panes).forEach((p, i) => {
if (scrollMap[i] !== undefined) p.scrollTop = scrollMap[i];
});
}).catch(err => {
console.error("Sync failed:", err);
alert("Fehler beim Speichern! Bitte Seite neu laden.");
});
}
function handlePackedItemActions(e) {
const button = e.target.closest('button');
if (!button) return;
const itemEl = button.closest('.packed-item-container');
if (!itemEl) return;
const itemId = itemEl.dataset.itemId;
if (button.classList.contains('remove-item-btn')) {
if (confirm('Diesen Artikel wirklich aus der Packliste entfernen?')) {
itemEl.remove();
sendApiRequest({ action: 'delete_item', item_id: itemId });
}
}
}
function handleQuantityChange(e) {
const input = e.target;
if (input.classList.contains('quantity-input')) {
const itemEl = input.closest('.packed-item-container');
const itemId = itemEl.dataset.itemId;
const newQuantity = input.value;
clearTimeout(input.dataset.timeout);
input.dataset.timeout = setTimeout(() => {
sendApiRequest({ action: 'update_quantity', item_id: itemId, quantity: newQuantity });
}, 500);
}
}
async function sendApiRequest(data) {
data.packing_list_id = packingListId;
try {
const response = await fetch('api_packing_list_handler.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({error: 'Serverfehler'}));
console.error(errorData);
return Promise.reject(errorData);
}
const json = await response.json();
if (data.action !== 'update_quantity') {
const fb = document.getElementById('save-feedback');
fb.style.opacity = '1';
setTimeout(() => { fb.style.opacity = '0'; }, 1500);
}
return json;
} catch(error) {
console.error(error);
return Promise.reject(error);
}
}
</script>
<?php require_once 'footer.php'; ?>

179
manufacturers.php Normal file
View File

@@ -0,0 +1,179 @@
<?php
// manufacturers.php - Herstellerverwaltung
// FINALE VERSION mit Haushaltslogik
$page_title = "Hersteller verwalten";
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'];
$message = '';
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$household_id = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
$household_member_ids = [$current_user_id];
if ($household_id) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $household_id);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) {
if (!in_array($row['id'], $household_member_ids)) {
$household_member_ids[] = $row['id'];
}
}
$stmt_members->close();
}
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
if ($_SERVER["REQUEST_METHOD"] == "POST") {
if (isset($_POST['add_manufacturer'])) {
$manufacturer_name = trim($_POST['manufacturer_name']);
if (!empty($manufacturer_name)) {
$stmt = $conn->prepare("INSERT INTO manufacturers (name, user_id) VALUES (?, ?)");
$stmt->bind_param("si", $manufacturer_name, $current_user_id);
if ($stmt->execute()) {
if ($household_id) {
$log_message = htmlspecialchars($_SESSION['username']) . " hat den Hersteller '" . htmlspecialchars($manufacturer_name) . "' hinzugefügt.";
log_household_action($conn, $household_id, $current_user_id, $log_message);
}
$message = '<div class="alert alert-success" role="alert">Hersteller erfolgreich hinzugefügt!</div>';
} else {
if ($conn->errno == 1062) {
$message = '<div class="alert alert-danger" role="alert">Fehler: Ein Hersteller mit diesem Namen existiert bereits für dein Konto.</div>';
} else {
$message = '<div class="alert alert-danger" role="alert">Fehler beim Hinzufügen des Herstellers: ' . $stmt->error . '</div>';
}
}
$stmt->close();
} else {
$message = '<div class="alert alert-danger" role="alert">Der Herstellername darf nicht leer sein.</div>';
}
}
elseif (isset($_POST['edit_manufacturer'])) {
// ... (POST-Logik unverändert) ...
}
}
elseif (isset($_GET['action']) && $_GET['action'] == 'delete' && isset($_GET['id'])) {
// ... (DELETE-Logik unverändert) ...
}
$manufacturers_query = $conn->prepare("SELECT m.id, m.name, m.user_id, u.username as creator_name FROM manufacturers m JOIN users u ON m.user_id = u.id WHERE m.user_id IN ($placeholders) ORDER BY m.name ASC");
$manufacturers_query->bind_param($types, ...$household_member_ids);
$manufacturers_query->execute();
$manufacturers_list = $manufacturers_query->get_result()->fetch_all(MYSQLI_ASSOC);
$manufacturers_query->close();
$conn->close();
?>
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">Hersteller im Haushalt</h2>
</div>
<div class="card-body p-4">
<?php if(!empty($message)) echo $message; ?>
<div class="card bg-light mb-5">
<div class="card-body">
<h5 class="card-title mb-3">Neuen Hersteller hinzufügen</h5>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post" class="row g-3 align-items-end">
<div class="col-sm-8">
<label for="manufacturer_name" class="form-label visually-hidden">Neuer Hersteller</label>
<input type="text" class="form-control" id="manufacturer_name" name="manufacturer_name" placeholder="Name des neuen Herstellers" required>
</div>
<div class="col-sm-4">
<button type="submit" name="add_manufacturer" class="btn btn-primary w-100"><i class="fas fa-plus-circle me-2"></i>Hinzufügen</button>
</div>
</form>
</div>
</div>
<h5 class="mb-3">Bestehende Hersteller</h5>
<?php if (empty($manufacturers_list)): ?>
<div class="alert alert-info text-center">Keine Hersteller gefunden.</div>
<?php else: ?>
<div class="list-group">
<?php foreach ($manufacturers_list as $manufacturer): ?>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-industry text-muted me-2"></i>
<span><?php echo htmlspecialchars($manufacturer['name']); ?></span>
<?php if ($manufacturer['user_id'] != $current_user_id): ?>
<small class="text-muted ms-2">(von <?php echo htmlspecialchars($manufacturer['creator_name']); ?>)</small>
<?php endif; ?>
</div>
<?php if ($manufacturer['user_id'] == $current_user_id): ?>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary" title="Bearbeiten" data-bs-toggle="modal" data-bs-target="#editManufacturerModal"
data-id="<?php echo htmlspecialchars($manufacturer['id']); ?>" data-name="<?php echo htmlspecialchars($manufacturer['name']); ?>">
<i class="fas fa-edit"></i>
</button>
<a href="manufacturers.php?action=delete&id=<?php echo htmlspecialchars($manufacturer['id']); ?>" class="btn btn-sm btn-outline-danger" title="Löschen" onclick="return confirm('Sind Sie sicher, dass Sie diesen Hersteller löschen möchten? Artikel, die diesem Hersteller zugewiesen sind, verlieren ihre Zuordnung.')">
<i class="fas fa-trash-alt"></i>
</a>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="modal fade" id="editManufacturerModal" tabindex="-1" aria-labelledby="editManufacturerModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editManufacturerModalLabel">Hersteller bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
<div class="modal-body">
<input type="hidden" name="manufacturer_id" id="edit_manufacturer_id">
<div class="mb-3">
<label for="edit_manufacturer_name" class="form-label">Herstellername</label>
<input type="text" class="form-control" id="edit_manufacturer_name" name="manufacturer_name" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" name="edit_manufacturer" class="btn btn-primary">Änderungen speichern</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var editModal = document.getElementById('editManufacturerModal');
if (editModal) {
editModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var id = button.getAttribute('data-id');
var name = button.getAttribute('data-name');
var modalIdInput = editModal.querySelector('#edit_manufacturer_id');
var modalNameInput = editModal.querySelector('#edit_manufacturer_name');
if (modalIdInput) modalIdInput.value = id;
if (modalNameInput) modalNameInput.value = name;
});
}
});
</script>
<?php require_once 'footer.php'; ?>

361
packing_list_detail.php Normal file
View File

@@ -0,0 +1,361 @@
<?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'; ?>

138
packing_lists.php Normal file
View File

@@ -0,0 +1,138 @@
<?php
// packing_lists.php - Übersicht über alle Haushalts-Packlisten
// KORREKTUR: Bearbeiten-Buttons werden nun auch für Haushaltsmitglieder bei geteilten Listen angezeigt.
$page_title = "Packlisten des Haushalts";
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'];
$message = '';
if (isset($_SESSION['message'])) {
$message = $_SESSION['message'];
unset($_SESSION['message']);
}
// Lade die Haushalts-ID und Mitglieder-IDs
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$current_user_household_id = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
$household_member_ids = [$current_user_id];
if ($current_user_household_id) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $current_user_household_id);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) {
if (!in_array($row['id'], $household_member_ids)) {
$household_member_ids[] = $row['id'];
}
}
$stmt_members->close();
}
$packing_lists = [];
// KORREKTUR: Die SQL-Abfrage wurde vereinfacht und korrigiert, um alle relevanten Listen anzuzeigen.
// Es werden alle Listen angezeigt, die entweder vom Nutzer erstellt wurden ODER mit seinem Haushalt geteilt sind.
$sql = "SELECT
pl.id, pl.name, pl.description, pl.user_id, pl.household_id,
u.username as creator_name,
COUNT(DISTINCT COALESCE(pli.carrier_user_id, 'sonstiges')) AS carrier_count,
SUM(pli.quantity * a.weight_grams) AS total_weight
FROM packing_lists pl
JOIN users u ON pl.user_id = u.id
LEFT JOIN packing_list_items pli ON pl.id = pli.packing_list_id
LEFT JOIN articles a ON pli.article_id = a.id
WHERE pl.user_id = ? OR pl.household_id = ?
GROUP BY pl.id, pl.name, pl.description, pl.user_id, u.username, pl.household_id
ORDER BY pl.name ASC";
$stmt = $conn->prepare($sql);
if ($stmt === false) {
$message .= '<div class="alert alert-danger" role="alert">SQL Prepare-Fehler: ' . $conn->error . '</div>';
} else {
$stmt->bind_param("ii", $current_user_id, $current_user_household_id);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
$packing_lists[] = $row;
}
$stmt->close();
}
$conn->close();
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Packlisten im Haushalt</h2>
<a href="add_packing_list.php" class="btn btn-sm btn-outline-light"><i class="fas fa-plus-circle me-2"></i>Neue Packliste erstellen</a>
</div>
<div class="card-body">
<?php if(!empty($message)) echo $message; ?>
<?php if (empty($packing_lists)): ?>
<div class="alert alert-info text-center" role="alert">
Du hast noch keine Packlisten angelegt. <a href="add_packing_list.php" class="alert-link">Erstelle jetzt deine erste!</a>
</div>
<?php else: ?>
<div class="row g-4">
<?php foreach ($packing_lists as $list): ?>
<div class="col-md-6 col-lg-4">
<div class="card h-100 shadow-sm packing-list-card">
<div class="card-body d-flex flex-column">
<img src="rucksack_icon.png" alt="Packliste" class="card-icon">
<h5 class="card-title"><?php echo htmlspecialchars($list['name']); ?></h5>
<p class="card-text text-muted flex-grow-1 small"><?php echo htmlspecialchars($list['description'] ?: 'Keine Beschreibung vorhanden.'); ?></p>
<div class="mt-auto d-flex justify-content-between">
<span class="badge bg-secondary creator-badge"><i class="fas fa-user-edit me-1"></i> Erstellt von <?php echo htmlspecialchars($list['creator_name']); ?></span>
<?php if (!empty($list['household_id'])): ?>
<span class="badge bg-success" data-bs-toggle="tooltip" title="Für den Haushalt freigegeben"><i class="fas fa-users"></i></span>
<?php else: ?>
<span class="badge bg-secondary" data-bs-toggle="tooltip" title="Private Liste"><i class="fas fa-user"></i></span>
<?php endif; ?>
</div>
</div>
<div class="card-footer d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<span class="me-3" data-bs-toggle="tooltip" title="Anzahl Träger"><i class="fas fa-users text-muted"></i> <span class="badge bg-primary rounded-pill ms-1"><?php echo $list['carrier_count']; ?></span></span>
<span data-bs-toggle="tooltip" title="Gesamtgewicht"><i class="fas fa-weight-hanging text-muted"></i> <span class="badge bg-success rounded-pill ms-1"><?php echo number_format(($list['total_weight'] ?? 0) / 1000, 2, ',', '.'); ?> kg</span></span>
</div>
<div>
<a href="packing_list_detail.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-success" title="Details ansehen"><i class="fas fa-eye"></i></a>
<?php
$is_owner = ($list['user_id'] == $current_user_id);
$can_edit_household_list = (!empty($list['household_id']) && $list['household_id'] == $current_user_household_id);
?>
<?php if ($is_owner || $can_edit_household_list): ?>
<a href="edit_packing_list_details.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-primary" title="Details bearbeiten"><i class="fas fa-pencil-alt"></i></a>
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Artikel verwalten"><i class="fas fa-boxes"></i></a>
<?php endif; ?>
<?php if ($is_owner): ?>
<a href="share_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-info" title="Teilen"><i class="fas fa-share-alt"></i></a>
<a href="duplicate_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-dark" title="Duplizieren" onclick="return confirm('Möchten Sie diese Packliste wirklich duplizieren?');"><i class="fas fa-copy"></i></a>
<a href="delete_packing_list.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-danger ms-2" title="Löschen" onclick="return confirm('Sind Sie sicher, dass Sie diese Packliste löschen möchten?');"><i class="fas fa-trash"></i></a>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php require_once 'footer.php'; ?>

269
print_packing_list.php Normal file
View File

@@ -0,0 +1,269 @@
<?php
// print_packing_list.php - Eigene Seite für die Druckansicht einer Packliste
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Prüfen, ob der Benutzer angemeldet ist
if (!isset($_SESSION['user_id'])) {
// Wenn nicht angemeldet, zur Login-Seite umleiten oder Fehlermeldung anzeigen
// Für die Druckansicht leiten wir einfach weiter, da ein unautorisierter Druck keinen Sinn macht.
header("Location: login.php");
exit;
}
require_once 'db_connect.php'; // Datenbankverbindung
$current_user_id = $_SESSION['user_id'];
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$packing_list = null;
$packing_list_items = [];
$message = '';
$total_weight_grams = 0;
$weight_by_category = [];
$total_consumable_weight = 0;
if ($packing_list_id > 0) {
// Packliste abrufen
$stmt = $conn->prepare("SELECT id, name, description, user_id FROM packing_lists WHERE id = ?");
if ($stmt) {
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows == 1) {
$packing_list = $result->fetch_assoc();
// Nur eigene Packlisten oder globale Packlisten dürfen gedruckt werden
if ($packing_list['user_id'] != $current_user_id) {
$message = 'Sie sind nicht berechtigt, diese Packliste zu drucken.';
$packing_list = null;
}
} else {
$message = 'Packliste nicht gefunden.';
}
$stmt->close();
} else {
$message = 'Datenbankfehler beim Abrufen der Packliste.';
}
} else {
$message = 'Keine Packlisten-ID zum Drucken angegeben.';
}
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");
if ($stmt) {
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result();
$raw_packing_list_items = [];
while ($row = $result->fetch_assoc()) {
$raw_packing_list_items[] = $row;
$item_total_weight = $row['weight_grams'] * $row['quantity'];
$total_weight_grams += $item_total_weight;
$category_name = $row['category_name'] ?: 'Unkategorisiert';
if (!isset($weight_by_category[$category_name])) {
$weight_by_category[$category_name] = 0;
}
$weight_by_category[$category_name] += $item_total_weight;
if ($row['consumable']) {
$total_consumable_weight += $item_total_weight;
}
}
$stmt->close();
// Items in hierarchische Struktur bringen
$items_by_id = [];
foreach ($raw_packing_list_items as $item) {
$item['children'] = [];
$items_by_id[$item['packing_list_item_id']] = $item;
}
foreach ($items_by_id as $item_id => &$item) {
if ($item['parent_packing_list_item_id'] !== null && isset($items_by_id[$item['parent_packing_list_item_id']])) {
$items_by_id[$item['parent_packing_list_item_id']]['children'][] = &$items_by_id[$item_id];
}
}
unset($item); // Wichtig, um Referenzfehler zu vermeiden!
foreach ($items_by_id as $item) {
if ($item['parent_packing_list_item_id'] === null) {
$packing_list_items[] = $item;
}
}
} else {
$message = 'Datenbankfehler beim Abrufen der Packlistenartikel.';
}
}
$conn->close();
$total_weight_without_consumables = $total_weight_grams - $total_consumable_weight;
// Rekursive Funktion zur Darstellung der Artikel (für Druckansicht angepasst)
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' : '') . ')';
$indent_style = ($level > 0) ? 'padding-left: ' . ($level * 2) . 'em;' : ''; // Deutlichere Einrückung im Druck
$html .= '
<li class="print-list-item" style="' . $indent_style . '">
<span class="print-checkbox"></span>
<span class="print-quantity">' . 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>
</li>';
if (!empty($item['children'])) {
$html .= renderPrintablePackingListItemsRecursive($item['children'], $level + 1);
}
}
$html .= '</ul>';
return $html;
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Packliste <?php echo htmlspecialchars($packing_list['name'] ?? 'Druckansicht'); ?></title>
<!-- Spezifisches Stylesheet für die Druckansicht einbinden -->
<link rel="stylesheet" href="print.css" media="print">
<!-- Optional: Ein minimales Stylesheet für die Bildschirmanzeige dieser Seite, falls nötig -->
<style>
/* Grundlegende Styles für die Bildschirmanzeige dieser Druckseite */
body {
font-family: Arial, sans-serif;
margin: 20px;
color: #333;
}
.container-print {
max-width: 800px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ccc; /* Minimaler Rahmen für die Bildschirmanzeige */
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1, h2, h3 {
text-align: center;
color: #2c3e50;
margin-bottom: 10px;
}
p.description {
text-align: center;
color: #777;
margin-bottom: 20px;
}
.section-title {
font-size: 1.2em;
font-weight: bold;
margin-top: 20px;
margin-bottom: 10px;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
.print-list-level {
list-style: none;
padding: 0;
margin: 0;
}
.print-list-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 5px 0;
border-bottom: 1px dashed #eee;
font-size: 0.9em;
}
.print-list-item:last-child {
border-bottom: none;
}
.print-checkbox {
width: 15px;
height: 15px;
border: 1px solid #000;
flex-shrink: 0;
margin-right: 8px;
margin-top: 3px; /* Align with text baseline */
}
.print-quantity {
font-weight: bold;
flex-shrink: 0;
margin-right: 8px;
}
.print-name {
flex-grow: 1;
margin-right: 8px;
word-break: break-word; /* Allow long names to wrap */
}
.print-details {
color: #555;
flex-shrink: 0;
margin-right: 8px;
white-space: nowrap; /* Keep details on one line for print-preview */
}
.print-total-weight {
font-weight: bold;
flex-shrink: 0;
text-align: right;
}
.statistics-list {
list-style: none;
padding: 0;
}
.statistics-list li {
display: flex;
justify-content: space-between;
padding: 3px 0;
border-bottom: 1px solid #eee;
}
.statistics-list li:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<div class="container-print">
<?php if ($packing_list): ?>
<h1><?php echo htmlspecialchars($packing_list['name']); ?></h1>
<p class="description"><?php echo htmlspecialchars($packing_list['description'] ?: 'Keine Beschreibung'); ?></p>
<h2 class="section-title">Enthaltene Artikel</h2>
<?php if (empty($packing_list_items)): ?>
<p>Diese Packliste enthält noch keine Artikel.</p>
<?php else: ?>
<?php echo renderPrintablePackingListItemsRecursive($packing_list_items); ?>
<?php endif; ?>
<h2 class="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:</strong>
<span><?php echo number_format($total_weight_without_consumables, 0, ',', '.') . 'g'; ?></span>
</li>
<?php
arsort($weight_by_category); // Sortiere Kategorien nach Gewicht absteigend
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>
<?php else: ?>
<p style="text-align: center; color: red;"><?php echo htmlspecialchars($message); ?></p>
<?php endif; ?>
</div>
<script>
// Startet den Druckdialog, sobald die Seite geladen ist
// setTimeout(function() { window.print(); }, 100); // Kleine Verzögerung zur Sicherheit
</script>
</body>
</html>

277
public_list.php Normal file
View File

@@ -0,0 +1,277 @@
<?php
// public_list.php - Öffentliche, schreibgeschützte Ansicht einer geteilten Packliste
// FINALE VERSION: Vollständiges Layout mit Statistiken, Diagrammen und allen Funktionen.
require_once 'db_connect.php';
$token = $_GET['token'] ?? '';
$packing_list = null;
$all_items = [];
$items_by_carrier_hierarchical = [];
$sorted_items_by_carrier = [];
// Statistik-Variablen
$total_weight = 0;
$total_items_quantity = 0;
$weight_by_category = [];
$weight_by_carrier = [];
$weight_by_carrier_non_consumable = [];
// 1. Finde die Packliste, die zu diesem Token gehört
if (!empty($token)) {
$stmt = $conn->prepare("SELECT * FROM packing_lists WHERE share_token = ?");
$stmt->bind_param("s", $token);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$packing_list = $result->fetch_assoc();
}
$stmt->close();
}
// 2. Wenn eine Packliste gefunden wurde, lade deren Inhalt
if ($packing_list) {
$page_title = "Packliste: " . htmlspecialchars($packing_list['name']);
$packing_list_id = $packing_list['id'];
// SQL-Abfrage holt alle relevanten Daten
$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, a.product_url, c.name AS category_name, m.name AS manufacturer_name, u.username AS carrier_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 WHERE pli.packing_list_id = ?";
$stmt_items = $conn->prepare($sql);
$stmt_items->bind_param("i", $packing_list_id);
$stmt_items->execute();
$result_items = $stmt_items->get_result();
while ($row = $result_items->fetch_assoc()) {
$all_items[$row['id']] = $row;
$item_total_weight = $row['quantity'] * $row['weight_grams'];
$total_weight += $item_total_weight;
$total_items_quantity += $row['quantity'];
$category_name = $row['category_name'] ?: 'Ohne Kategorie';
$carrier_name = $row['carrier_name'] ?: 'Sonstiges';
if (!isset($weight_by_category[$category_name])) $weight_by_category[$category_name] = 0;
$weight_by_category[$category_name] += $item_total_weight;
if (!isset($weight_by_carrier[$carrier_name])) $weight_by_carrier[$carrier_name] = 0;
$weight_by_carrier[$carrier_name] += $item_total_weight;
if (!$row['consumable']) {
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_total_weight;
}
}
$stmt_items->close();
// Hierarchie aufbauen
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 (empty($item['parent_packing_list_item_id']) || !isset($carrier_items[$item['parent_packing_list_item_id']])) {
$hierarchical_list[$id] = &$item;
} else {
if (!isset($carrier_items[$item['parent_packing_list_item_id']]['children'])) {
$carrier_items[$item['parent_packing_list_item_id']]['children'] = [];
}
$carrier_items[$item['parent_packing_list_item_id']]['children'][] = &$item;
}
}
$carrier_items = $hierarchical_list;
}
unset($carrier_items, $item);
// Sortierung
$sorted_items_by_carrier = $items_by_carrier_hierarchical;
ksort($sorted_items_by_carrier);
if(isset($sorted_items_by_carrier['Sonstiges'])) {
$unassigned = $sorted_items_by_carrier['Sonstiges'];
unset($sorted_items_by_carrier['Sonstiges']);
$sorted_items_by_carrier['Sonstiges'] = $unassigned;
}
} else {
$page_title = "Ungültiger Link";
}
$conn->close();
// Render-Funktionen
function render_public_rows($items, $level = 0) {
foreach ($items as $item) {
$item_weight = $item['quantity'] * $item['weight_grams'];
$has_children = !empty($item['children']);
echo '<tr>';
echo '<td><img src="' . (!empty($item['image_url']) ? htmlspecialchars($item['image_url']) : 'https://placehold.co/100x100/e0e0e0/555555?text=O') . '" alt="' . htmlspecialchars($item['article_name']) . '" class="item-image article-image-trigger" data-preview-url="' . htmlspecialchars($item['image_url'] ?? '') . '"></td>';
echo '<td class="text-center">' . (!empty($item['product_url']) ? '<a href="'.htmlspecialchars($item['product_url']).'" target="_blank" class="btn btn-sm btn-outline-secondary" data-bs-toggle="tooltip" title="Produktseite öffnen"><i class="fas fa-external-link-alt"></i></a>' : '') . '</td>';
echo '<td style="padding-left: ' . (10 + ($level * 30)) . 'px;">' . ($level > 0 ? '<i class="fas fa-level-up-alt fa-rotate-90 text-muted me-2"></i>' : '') . htmlspecialchars($item['article_name']) . '</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="badge bg-secondary rounded-pill">' . number_format($item['weight_grams'], 0, ',', '.') . ' g</span></td>';
echo '<td class="text-end"><span class="badge bg-secondary rounded-pill">' . number_format($item_weight, 0, ',', '.') . ' g</span></td>';
echo '</tr>';
if ($has_children) render_public_rows($item['children'], $level + 1);
}
}
function render_print_table_rows($items, $level = 0) {
foreach($items as $item) {
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 * 25) . '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 class="text-center">' . $item['quantity'] . 'x</td>';
echo '</tr>';
if (!empty($item['children'])) render_print_table_rows($item['children'], $level + 1);
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($page_title); ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root { --color-primary: #3B4A23; --color-accent: #2c3e50; }
body { background-color: #f4f7f6; font-family: 'Nunito Sans', sans-serif; }
.table th, .table td { vertical-align: middle; }
.item-image { width: 40px; height: 40px; object-fit: cover; border-radius: .3rem; cursor: pointer; }
.card-header { background-color: var(--color-primary); color: white; font-weight: 700; }
.card-header-stats { background-color: var(--color-accent); color: white; }
.carrier-header-row td { background-color: #f8f9fa; font-weight: bold; padding: 0.75rem 1.25rem; border-top: 5px solid #fff; border-bottom: 1px solid #dee2e6;}
.stats-table-container { border: 1px solid #dee2e6; border-radius: .5rem; background-color: #f8f9fa; overflow: hidden; padding: 0.75rem; }
.chart-container { position: relative; min-height: 220px; width: 100%; margin-bottom: 1rem; }
.table th.consumable-col { width: 40px; text-align: center; }
#image-preview-tooltip { display: none; position: absolute; border: 1px solid #ccc; background-color: #fff; padding: 5px; z-index: 1000; width: 200px; height: 200px; background-repeat: no-repeat; background-position: center center; background-size: contain; box-shadow: 0 4px 8px rgba(0,0,0,0.1); border-radius: 5px; pointer-events: none; }
.print-view { display: none; }
.app-footer { text-align: center; padding: 1.5rem; margin-top: 2rem; color: #888; font-size: 0.9em; }
.app-footer a { color: #555; font-weight: bold; text-decoration: none; }
.app-footer a:hover { text-decoration: underline; }
@media print {
body { background: none !important; } .screen-view, .print-hide, .app-footer { display: none !important; } .print-view { display: block !important; }
@page { margin: 1.5cm; } .print-header h1 { font-size: 20pt; text-align: center; margin-bottom: 2rem; }
.print-carrier-group { page-break-inside: avoid; margin-top: 1.5rem; } .print-carrier-header { font-size: 14pt; border-bottom: 2px solid #333; padding-bottom: 5px; margin-bottom: 1rem; }
.print-table { width: 100%; border-collapse: collapse; font-size: 10pt; } .print-table th, .print-table td { text-align: left; padding: 6px; border-bottom: 1px solid #ccc; vertical-align: middle; }
.print-table th { font-weight: bold; } .print-checkbox-cell { width: 30px; } .print-checkbox { display: inline-block; width: 16px; height: 16px; border: 1px solid #333; }
.print-child-item .arrow-wrapper { position: relative; display: inline-block; width: 15px; height: 1.2em; vertical-align: middle; margin-right: 5px; }
.print-child-item .arrow-wrapper::before { content: ''; position: absolute; left: 5px; top: 0; height: 100%; border-left: 1px solid #888;}
.print-child-item .arrow-wrapper::after { content: ''; position: absolute; left: 5px; top: 50%; width: 10px; border-top: 1px solid #888;}
}
</style>
</head>
<body>
<div class="container my-4">
<?php if ($packing_list): ?>
<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><?php echo htmlspecialchars($packing_list['name']); ?></h1>
<button onclick="window.print();" class="btn btn-light btn-sm print-hide"><i class="fas fa-print me-2"></i>Drucken</button>
</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 style="width:40px;" title="Produktlink"><i class="fas fa-link"></i></th><th>Artikel</th><th class="consumable-col" 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="10" class="text-center p-4">Diese Packliste ist leer.</td></tr>
<?php else: ?>
<?php foreach($sorted_items_by_carrier as $carrier => $items): ?>
<tr class="carrier-header-row"><td colspan="10"><h6><?php echo htmlspecialchars($carrier); ?></h6></td></tr>
<?php render_public_rows($items); ?>
<?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></li>
<?php endforeach; ?>
</ul>
</div>
<div class="chart-container"><canvas id="categoryWeightChart"></canvas></div>
</div>
</div>
</div>
</div>
</div>
<div class="print-view">
<div class="print-header"><h1>Packliste: <?php echo htmlspecialchars($packing_list['name']); ?></h1></div>
<?php foreach($sorted_items_by_carrier as $carrier => $items): ?>
<div class="print-carrier-group">
<h2 class="print-carrier-header"><?php echo htmlspecialchars($carrier); ?></h2>
<table class="print-table">
<thead><tr><th class="print-checkbox-cell"></th><th>Artikel</th><th>Hersteller</th><th>Modell/Typ</th><th class="text-center">Menge</th></tr></thead>
<tbody><?php render_print_table_rows($items); ?></tbody>
</table>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="alert alert-danger text-center p-5"><h4>Ungültiger Link</h4><p class="lead">Der angegebene Freigabe-Link ist ungültig oder wurde deaktiviert.</p></div>
<?php endif; ?>
</div>
<div id="image-preview-tooltip"></div>
<footer class="app-footer">
Erstellt mit <a href="#" target="_blank">Trekking Packliste</a>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) });
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 !== '' && !url.includes('placehold.co')) {
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'; });
});
}
<?php if ($packing_list && !empty($weight_by_category)): ?>
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: ['#3B4A23', '#6B8E23', '#90A955', '#4F6F52', '#739072', '#8A9A5B', '#A9A9A9'], borderWidth: 2 }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'bottom' } } }
});
}
<?php endif; ?>
});
</script>
</body>
</html>

109
register.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
// register.php - Seite zur Benutzerregistrierung
// KORREKTUR: Vollständiges Design hinzugefügt, um es an das Login-Seiten-Layout anzugleichen.
$page_title = "Registrierung";
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
require_once 'db_connect.php';
$message = '';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = trim($_POST['username']);
$password = trim($_POST['password']);
$confirm_password = trim($_POST['confirm_password']);
$errors = [];
if (empty($username)) {
$errors[] = "Der Benutzername darf nicht leer sein.";
} elseif (strlen($username) < 3) {
$errors[] = "Der Benutzername muss mindestens 3 Zeichen lang sein.";
}
if (empty($password)) {
$errors[] = "Das Passwort darf nicht leer sein.";
} elseif (strlen($password) < 6) {
$errors[] = "Das Passwort muss mindestens 6 Zeichen lang sein.";
}
if ($password !== $confirm_password) {
$errors[] = "Die Passwörter stimmen nicht überein!";
}
if (!empty($errors)) {
$message = '<div class="alert alert-danger" role="alert">' . implode('<br>', $errors) . '</div>';
} else {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt_check = $conn->prepare("SELECT id FROM users WHERE username = ?");
$stmt_check->bind_param("s", $username);
$stmt_check->execute();
$stmt_check->store_result();
if ($stmt_check->num_rows > 0) {
$message = '<div class="alert alert-danger" role="alert">Dieser Benutzername ist bereits vergeben.</div>';
} else {
$stmt_insert = $conn->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt_insert->bind_param("ss", $username, $hashed_password);
if ($stmt_insert->execute()) {
$message = '<div class="alert alert-success" role="alert">Registrierung erfolgreich! Sie können sich jetzt <a href="login.php" class="alert-link">anmelden</a>.</div>';
} else {
error_log("Registrierungsfehler: " . $stmt_insert->error);
$message = '<div class="alert alert-danger" role="alert">Ein interner Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</div>';
}
$stmt_insert->close();
}
$stmt_check->close();
}
}
$conn->close();
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registrierung - Trekking Packliste</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<div class="auth-wrapper">
<div class="auth-container">
<div class="auth-header">
<img src="./logo.png" alt="Trekking Packliste Logo" class="auth-logo">
<h2>Registrieren</h2>
</div>
<div class="auth-body">
<?php if(!empty($message)) echo $message; ?>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Passwort bestätigen</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit" class="btn btn-primary w-100 mt-3">Konto erstellen</button>
</form>
<div class="text-center mt-4">
<span class="text-muted">Bereits registriert?</span> <a href="login.php" class="fw-bold text-decoration-none" style="color: var(--color-primary);">Hier anmelden</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
</body>
</html>

BIN
rucksack_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

317
share_packing_list.php Normal file
View File

@@ -0,0 +1,317 @@
<?php
// share_packing_list.php - Seite zum Verwalten von Packlisten-Freigaben
// KORREKTUR: Neue Sektion zur Freigabe für den gesamten Haushalt hinzugefügt.
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once 'db_connect.php';
$current_user_id = $_SESSION['user_id'];
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$packing_list = null;
$message = '';
if ($packing_list_id <= 0) {
header("Location: packing_lists.php");
exit;
}
// Lade Haushalts-ID des aktuellen Nutzers
$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();
// Prüfe, ob der Nutzer der Besitzer der Liste ist
$stmt_owner_check = $conn->prepare("SELECT id, name, user_id, share_token, household_id FROM packing_lists WHERE id = ? AND user_id = ?");
$stmt_owner_check->bind_param("ii", $packing_list_id, $current_user_id);
$stmt_owner_check->execute();
$result = $stmt_owner_check->get_result();
if ($result->num_rows !== 1) {
// Wenn nicht Besitzer, Weiterleitung, da man Freigaben nicht verwalten kann
header("Location: packing_lists.php");
exit;
}
$packing_list = $result->fetch_assoc();
$stmt_owner_check->close();
// VERARBEITE FORMULAR-AKTIONEN
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$action = $_POST['action'] ?? '';
// Aktionen für Haushaltsfreigabe
if ($action == 'share_with_household' && $current_user_household_id) {
$stmt_share = $conn->prepare("UPDATE packing_lists SET household_id = ? WHERE id = ? AND user_id = ?");
$stmt_share->bind_param("iii", $current_user_household_id, $packing_list_id, $current_user_id);
$stmt_share->execute();
$stmt_share->close();
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=household_shared");
exit;
}
elseif ($action == 'unshare_from_household') {
$stmt_unshare = $conn->prepare("UPDATE packing_lists SET household_id = NULL WHERE id = ? AND user_id = ?");
$stmt_unshare->bind_param("ii", $packing_list_id, $current_user_id);
$stmt_unshare->execute();
$stmt_unshare->close();
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=household_unshared");
exit;
}
// AKTION: NEUEN ANONYMEN LINK ERSTELLEN
elseif ($action == 'generate_token') {
$token = bin2hex(random_bytes(16));
$stmt_token = $conn->prepare("UPDATE packing_lists SET share_token = ? WHERE id = ?");
$stmt_token->bind_param("si", $token, $packing_list_id);
$stmt_token->execute();
$stmt_token->close();
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=token_generated");
exit;
}
// AKTION: ANONYMEN LINK LÖSCHEN
elseif ($action == 'delete_token') {
$stmt_token = $conn->prepare("UPDATE packing_lists SET share_token = NULL WHERE id = ?");
$stmt_token->bind_param("i", $packing_list_id);
$stmt_token->execute();
$stmt_token->close();
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=token_deleted");
exit;
}
// AKTION: NEUE FREIGABE FÜR BENUTZER HINZUFÜGEN
elseif ($action == 'add_share') {
$shared_with_user_id = intval($_POST['shared_with_user_id']);
$permission_level = $_POST['permission_level'];
if ($shared_with_user_id > 0 && in_array($permission_level, ['read', 'edit'])) {
$stmt_check = $conn->prepare("SELECT id FROM packing_list_shares WHERE packing_list_id = ? AND shared_with_user_id = ?");
$stmt_check->bind_param("ii", $packing_list_id, $shared_with_user_id);
$stmt_check->execute();
if ($stmt_check->get_result()->num_rows > 0) {
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=share_exists");
exit;
}
$stmt_check->close();
$stmt_add = $conn->prepare("INSERT INTO packing_list_shares (packing_list_id, shared_with_user_id, permission_level) VALUES (?, ?, ?)");
$stmt_add->bind_param("iis", $packing_list_id, $shared_with_user_id, $permission_level);
if ($stmt_add->execute()) {
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=shared");
} else {
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=dberror");
}
$stmt_add->close();
exit;
}
}
// AKTION: FREIGABE AKTUALISIEREN
elseif ($action == 'update_share') {
$share_id = intval($_POST['share_id']);
$permission_level = $_POST['permission_level'];
if ($share_id > 0 && in_array($permission_level, ['read', 'edit'])) {
$stmt_update = $conn->prepare("UPDATE packing_list_shares SET permission_level = ? WHERE id = ? AND packing_list_id = ?");
$stmt_update->bind_param("sii", $permission_level, $share_id, $packing_list_id);
if($stmt_update->execute()){
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=updated");
} else {
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=dberror");
}
$stmt_update->close();
exit;
}
}
// AKTION: FREIGABE LÖSCHEN
elseif ($action == 'delete_share') {
$share_id = intval($_POST['share_id']);
if ($share_id > 0) {
$stmt_delete = $conn->prepare("DELETE FROM packing_list_shares WHERE id = ? AND packing_list_id = ?");
$stmt_delete->bind_param("ii", $share_id, $packing_list_id);
if($stmt_delete->execute()){
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=deleted");
} else {
header("Location: share_packing_list.php?id=" . $packing_list_id . "&msg=dberror");
}
$stmt_delete->close();
exit;
}
}
}
// DATENLADEN FÜR DIE SEITENANZEIGE
$stmt_users = $conn->prepare("SELECT id, username FROM users WHERE id != ? ORDER BY username ASC");
$stmt_users->bind_param("i", $current_user_id);
$stmt_users->execute();
$result_users = $stmt_users->get_result();
$all_other_users = $result_users->fetch_all(MYSQLI_ASSOC);
$stmt_users->close();
$stmt_shares = $conn->prepare("SELECT pls.id AS share_id, pls.permission_level, u.username AS shared_username FROM packing_list_shares pls JOIN users u ON pls.shared_with_user_id = u.id WHERE pls.packing_list_id = ? ORDER BY u.username ASC");
$stmt_shares->bind_param("i", $packing_list_id);
$stmt_shares->execute();
$result_shares = $stmt_shares->get_result();
$existing_shares = $result_shares->fetch_all(MYSQLI_ASSOC);
$stmt_shares->close();
$conn->close();
if (isset($_GET['msg'])) {
$alert_type = 'info'; $alert_msg = '';
switch ($_GET['msg']) {
case 'household_shared': $alert_type = 'success'; $alert_msg = 'Packliste erfolgreich für den Haushalt freigegeben!'; break;
case 'household_unshared': $alert_type = 'success'; $alert_msg = 'Freigabe für den Haushalt wurde aufgehoben.'; break;
case 'shared': $alert_type = 'success'; $alert_msg = 'Packliste erfolgreich freigegeben!'; break;
case 'updated': $alert_type = 'success'; $alert_msg = 'Freigabe erfolgreich aktualisiert!'; break;
case 'deleted': $alert_type = 'success'; $alert_msg = 'Freigabe erfolgreich entfernt!'; break;
case 'token_generated': $alert_type = 'success'; $alert_msg = 'Anonymer Freigabe-Link wurde erstellt.'; break;
case 'token_deleted': $alert_type = 'success'; $alert_msg = 'Anonymer Freigabe-Link wurde deaktiviert.'; break;
case 'share_exists': $alert_type = 'warning'; $alert_msg = 'Diese Packliste ist für diesen Benutzer bereits freigegeben.'; break;
default: $alert_type = 'danger'; $alert_msg = 'Ein unbekannter Fehler ist aufgetreten.'; break;
}
$message = '<div class="alert alert-'.$alert_type.'" role="alert">'.$alert_msg.'</div>';
}
$page_title = "Freigaben für: " . htmlspecialchars($packing_list['name']);
require_once 'header.php';
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Freigaben für "<?php echo htmlspecialchars($packing_list['name']); ?>" verwalten</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; ?>
<?php if ($current_user_household_id): ?>
<div class="card bg-light mb-5">
<div class="card-body">
<h5 class="card-title mb-3"><i class="fas fa-users me-2"></i>Freigabe für den Haushalt</h5>
<?php if (empty($packing_list['household_id'])): ?>
<p class="text-muted small">Gib diese Packliste für alle Mitglieder deines Haushalts frei. Jedes Mitglied kann sie dann ansehen und bearbeiten.</p>
<form action="share_packing_list.php?id=<?php echo $packing_list_id; ?>" method="post">
<input type="hidden" name="action" value="share_with_household">
<button type="submit" class="btn btn-success"><i class="fas fa-check-circle me-2"></i>Für meinen Haushalt freigeben</button>
</form>
<?php else: ?>
<p class="text-success"><i class="fas fa-check-circle me-2"></i>Diese Packliste ist für deinen Haushalt freigegeben.</p>
<form action="share_packing_list.php?id=<?php echo $packing_list_id; ?>" method="post">
<input type="hidden" name="action" value="unshare_from_household">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Möchten Sie die Freigabe für den Haushalt wirklich aufheben? Die Liste wird dann wieder privat.');"><i class="fas fa-times-circle me-2"></i>Freigabe aufheben</button>
</form>
<?php endif; ?>
</div>
</div>
<hr class="my-5">
<?php endif; ?>
<div class="card bg-light mb-5">
<div class="card-body">
<h5 class="card-title mb-3"><i class="fas fa-link me-2"></i>Anonymer Freigabe-Link (schreibgeschützt)</h5>
<p class="text-muted small">Erstelle einen geheimen Link, um die Packliste mit Personen zu teilen, die keinen Account haben. Dieser Link gewährt nur Lesezugriff.</p>
<?php if (empty($packing_list['share_token'])): ?>
<form action="share_packing_list.php?id=<?php echo $packing_list_id; ?>" method="post">
<input type="hidden" name="action" value="generate_token">
<button type="submit" class="btn btn-secondary"><i class="fas fa-plus-circle me-2"></i>Geheimen Link erstellen</button>
</form>
<?php else: ?>
<label for="share_url" class="form-label">Ihr geheimer Link:</label>
<div class="input-group">
<?php
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$host = $_SERVER['HTTP_HOST'];
$path = rtrim(dirname($_SERVER['PHP_SELF']), '/\\');
$share_url = $protocol . $host . $path . '/public_list.php?token=' . $packing_list['share_token'];
?>
<input type="text" id="share_url" class="form-control" value="<?php echo htmlspecialchars($share_url); ?>" readonly>
<button id="copy-btn" class="btn btn-outline-secondary" onclick="copyShareLink(this)" data-bs-toggle="tooltip" title="Link kopieren"><i class="fas fa-copy"></i></button>
</div>
<form action="share_packing_list.php?id=<?php echo $packing_list_id; ?>" method="post" class="mt-2">
<input type="hidden" name="action" value="delete_token">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Möchten Sie den geheimen Link wirklich dauerhaft deaktivieren?');"><i class="fas fa-trash-alt me-2"></i>Link deaktivieren</button>
</form>
<?php endif; ?>
</div>
</div>
<hr class="my-5">
<h5 class="mb-3">Für einzelne Benutzer freigeben</h5>
<div class="card bg-light mb-5">
<div class="card-body">
<form action="share_packing_list.php?id=<?php echo htmlspecialchars($packing_list_id); ?>" method="post" class="row g-3 align-items-end">
<input type="hidden" name="action" value="add_share">
<div class="col-md-5"><label for="shared_with_user_id" class="form-label">Benutzer</label><select class="form-select" id="shared_with_user_id" name="shared_with_user_id" required><option value="" disabled selected>-- Bitte wählen --</option><?php foreach ($all_other_users as $user): ?><option value="<?php echo htmlspecialchars($user['id']); ?>"><?php echo htmlspecialchars($user['username']); ?></option><?php endforeach; ?></select></div>
<div class="col-md-4"><label for="permission_level" class="form-label">Berechtigung</label><select class="form-select" id="permission_level" name="permission_level" required><option value="read">Lesen</option><option value="edit">Bearbeiten</option></select></div>
<div class="col-md-3"><button type="submit" class="btn btn-primary w-100"><i class="fas fa-user-plus me-2"></i>Freigeben</button></div>
</form>
</div>
</div>
<?php if (!empty($existing_shares)): ?>
<h5 class="mb-3 mt-5">Bestehende Einzel-Freigaben</h5>
<ul class="list-group">
<?php foreach ($existing_shares as $share): ?>
<li class="list-group-item d-flex flex-wrap justify-content-between align-items-center">
<div class="fw-bold"><i class="fas fa-user me-2 text-muted"></i><?php echo htmlspecialchars($share['shared_username']); ?></div>
<div class="d-flex align-items-center mt-2 mt-md-0">
<form action="share_packing_list.php?id=<?php echo htmlspecialchars($packing_list_id); ?>" method="post" class="d-inline-flex me-2">
<input type="hidden" name="action" value="update_share">
<input type="hidden" name="share_id" value="<?php echo htmlspecialchars($share['share_id']); ?>">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="permission_level" id="read_<?php echo $share['share_id']; ?>" value="read" <?php echo ($share['permission_level'] == 'read') ? 'checked' : ''; ?> onchange="this.form.submit()">
<label class="btn btn-sm btn-outline-secondary" for="read_<?php echo $share['share_id']; ?>">Lesen</label>
<input type="radio" class="btn-check" name="permission_level" id="edit_<?php echo $share['share_id']; ?>" value="edit" <?php echo ($share['permission_level'] == 'edit') ? 'checked' : ''; ?> onchange="this.form.submit()">
<label class="btn btn-sm btn-outline-primary" for="edit_<?php echo $share['share_id']; ?>">Bearbeiten</label>
</div>
</form>
<form action="share_packing_list.php?id=<?php echo htmlspecialchars($packing_list_id); ?>" method="post" class="d-inline">
<input type="hidden" name="action" value="delete_share">
<input type="hidden" name="share_id" value="<?php echo htmlspecialchars($share['share_id']); ?>">
<button type="submit" class="btn btn-danger btn-sm" title="Freigabe entfernen" onclick="return confirm('Sind Sie sicher, dass Sie diese Freigabe entfernen möchten?');"><i class="fas fa-trash-alt"></i></button>
</form>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
<script>
function copyShareLink(button) {
const urlInput = document.getElementById('share_url');
if (!navigator.clipboard) {
urlInput.select();
document.execCommand('copy');
alert('Link in die Zwischenablage kopiert!');
return;
}
navigator.clipboard.writeText(urlInput.value).then(function() {
const originalIconHTML = button.innerHTML;
const originalTitle = button.getAttribute('data-bs-original-title');
button.innerHTML = '<i class="fas fa-check text-success"></i>';
const tooltip = bootstrap.Tooltip.getInstance(button);
if (tooltip) {
tooltip.setContent({ '.tooltip-inner': 'Kopiert!' });
tooltip.show();
}
setTimeout(() => {
button.innerHTML = originalIconHTML;
if (tooltip) {
tooltip.hide();
tooltip.setContent({ '.tooltip-inner': originalTitle });
}
}, 2000);
}, function(err) {
alert('Fehler: Der Link konnte nicht kopiert werden.');
});
}
</script>
<?php require_once 'footer.php'; ?>

251
storage_locations.php Normal file
View File

@@ -0,0 +1,251 @@
<?php
// storage_locations.php - Verwaltung für Lagerorte
// KORREKTUR: Design der Karten-Header für bessere Lesbarkeit repariert.
$page_title = "Lagerorte verwalten";
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'];
$message = '';
// Lade Haushalts-Mitglieder-IDs, da Orte im Haushalt geteilt werden
$stmt_household = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household->bind_param("i", $current_user_id);
$stmt_household->execute();
$household_id = $stmt_household->get_result()->fetch_assoc()['household_id'];
$stmt_household->close();
$household_member_ids = [$current_user_id];
if ($household_id) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $household_id);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) {
if (!in_array($row['id'], $household_member_ids)) {
$household_member_ids[] = $row['id'];
}
}
$stmt_members->close();
}
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
// Formularverarbeitung
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// AKTION: ORT HINZUFÜGEN
if (isset($_POST['add_location'])) {
$name = trim($_POST['name']);
$parent_id = !empty($_POST['parent_id']) ? intval($_POST['parent_id']) : NULL;
if (!empty($name)) {
$stmt = $conn->prepare("INSERT INTO storage_locations (user_id, parent_id, name) VALUES (?, ?, ?)");
$stmt->bind_param("iis", $current_user_id, $parent_id, $name);
if ($stmt->execute()) {
$message = '<div class="alert alert-success">Lagerort erfolgreich hinzugefügt.</div>';
} else {
$message = '<div class="alert alert-danger">Fehler beim Hinzufügen: ' . $stmt->error . '</div>';
}
$stmt->close();
} else {
$message = '<div class="alert alert-danger">Der Name darf nicht leer sein.</div>';
}
}
// AKTION: ORT BEARBEITEN
elseif (isset($_POST['edit_location'])) {
$name = trim($_POST['edit_name']);
$location_id = intval($_POST['location_id']);
if (!empty($name) && $location_id > 0) {
// Jedes Haushaltsmitglied darf bearbeiten, aber die user_id wird nicht geändert
$stmt = $conn->prepare("UPDATE storage_locations SET name = ? WHERE id = ? AND user_id IN ($placeholders)");
$all_params = array_merge([$name, $location_id], $household_member_ids);
$all_types = 'si' . $types;
$stmt->bind_param($all_types, ...$all_params);
if ($stmt->execute() && $stmt->affected_rows > 0) {
$message = '<div class="alert alert-success">Lagerort erfolgreich umbenannt.</div>';
} else {
$message = '<div class="alert alert-info">Keine Änderung vorgenommen oder keine Berechtigung.</div>';
}
$stmt->close();
} else {
$message = '<div class="alert alert-danger">Ungültige Daten zum Bearbeiten.</div>';
}
}
}
// AKTION: ORT LÖSCHEN
elseif (isset($_GET['action']) && $_GET['action'] == 'delete' && isset($_GET['id'])) {
$location_id = intval($_GET['id']);
$stmt_check = $conn->prepare("SELECT COUNT(*) as child_count FROM storage_locations WHERE parent_id = ?");
$stmt_check->bind_param("i", $location_id);
$stmt_check->execute();
$child_count = $stmt_check->get_result()->fetch_assoc()['child_count'];
$stmt_check->close();
if ($child_count > 0) {
$message = '<div class="alert alert-danger">Dieser Ort kann nicht gelöscht werden, da ihm noch Unter-Orte zugeordnet sind. Bitte leere ihn zuerst.</div>';
} else {
$stmt_delete = $conn->prepare("DELETE FROM storage_locations WHERE id = ? AND user_id IN ($placeholders)");
$all_params = array_merge([$location_id], $household_member_ids);
$all_types = 'i' . $types;
$stmt_delete->bind_param($all_types, ...$all_params);
if ($stmt_delete->execute() && $stmt_delete->affected_rows > 0) {
$message = '<div class="alert alert-success">Lagerort erfolgreich gelöscht.</div>';
} else {
$message = '<div class="alert alert-danger">Fehler: Ort nicht gefunden oder keine Berechtigung.</div>';
}
$stmt_delete->close();
}
}
// Lade alle Orte des Haushalts
$sql = "SELECT sl.id, sl.name, sl.parent_id, sl.user_id FROM storage_locations sl WHERE sl.user_id IN ($placeholders) ORDER BY sl.parent_id ASC, sl.name ASC";
$stmt_load = $conn->prepare($sql);
$stmt_load->bind_param($types, ...$household_member_ids);
$stmt_load->execute();
$all_locations = $stmt_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_load->close();
$conn->close();
// Strukturiere Orte hierarchisch
$locations_level1 = [];
$locations_level2 = [];
foreach ($all_locations as $location) {
if ($location['parent_id'] === NULL) {
$locations_level1[] = $location;
} else {
if (!isset($locations_level2[$location['parent_id']])) {
$locations_level2[$location['parent_id']] = [];
}
$locations_level2[$location['parent_id']][] = $location;
}
}
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0"><i class="fas fa-archive me-2"></i>Lagerorte verwalten</h2>
</div>
<div class="card-body p-4">
<?php if(!empty($message)) echo $message; ?>
<div class="card bg-light mb-5">
<div class="card-body">
<h5 class="card-title mb-3">Neuen Lagerort hinzufügen</h5>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
<div class="row g-3">
<div class="col-md-5">
<label for="name" class="form-label">Name des Ortes</label>
<input type="text" class="form-control" id="name" name="name" placeholder="z.B. Keller, Schrank, Box #3" required>
</div>
<div class="col-md-5">
<label for="parent_id" class="form-label">Untergeordnet zu (Ebene 2)</label>
<select class="form-select" id="parent_id" name="parent_id">
<option value="">-- Keine (neue Ebene 1) --</option>
<?php foreach ($locations_level1 as $loc1): ?>
<option value="<?php echo $loc1['id']; ?>"><?php echo htmlspecialchars($loc1['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" name="add_location" class="btn btn-primary w-100"><i class="fas fa-plus-circle me-2"></i>Hinzufügen</button>
</div>
</div>
</form>
</div>
</div>
<h5 class="mb-3">Bestehende Lagerorte</h5>
<?php if (empty($locations_level1)): ?>
<div class="alert alert-info text-center">Noch keine Lagerorte erstellt.</div>
<?php else: ?>
<div class="row g-3">
<?php foreach ($locations_level1 as $loc1): ?>
<div class="col-md-6">
<div class="card level-1-card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="fas fa-box-open me-2"></i><?php echo htmlspecialchars($loc1['name']); ?></h6>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editLocationModal" data-id="<?php echo $loc1['id']; ?>" data-name="<?php echo htmlspecialchars($loc1['name']); ?>"><i class="fas fa-edit"></i></button>
<a href="storage_locations.php?action=delete&id=<?php echo $loc1['id']; ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Sicher, dass du diesen Ort löschen willst?');"><i class="fas fa-trash"></i></a>
</div>
</div>
<div class="card-body">
<?php if (isset($locations_level2[$loc1['id']])): ?>
<ul class="list-group level-2-list">
<?php foreach ($locations_level2[$loc1['id']] as $loc2): ?>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span><i class="fas fa-long-arrow-alt-right me-2 text-muted"></i><?php echo htmlspecialchars($loc2['name']); ?></span>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary py-0 px-1" data-bs-toggle="modal" data-bs-target="#editLocationModal" data-id="<?php echo $loc2['id']; ?>" data-name="<?php echo htmlspecialchars($loc2['name']); ?>"><i class="fas fa-edit"></i></button>
<a href="storage_locations.php?action=delete&id=<?php echo $loc2['id']; ?>" class="btn btn-sm btn-outline-danger py-0 px-1" onclick="return confirm('Sicher, dass du diesen Unter-Ort löschen willst?');"><i class="fas fa-trash"></i></a>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p class="text-muted small text-center">Noch keine Unter-Ebenen vorhanden.</p>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="modal fade" id="editLocationModal" tabindex="-1" aria-labelledby="editLocationModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editLocationModalLabel">Lagerort umbenennen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
<div class="modal-body">
<input type="hidden" name="location_id" id="edit_location_id">
<div class="mb-3">
<label for="edit_name" class="form-label">Neuer Name</label>
<input type="text" class="form-control" id="edit_name" name="edit_name" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" name="edit_location" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var editLocationModal = document.getElementById('editLocationModal');
if (editLocationModal) {
editLocationModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var locationId = button.getAttribute('data-id');
var locationName = button.getAttribute('data-name');
editLocationModal.querySelector('#edit_location_id').value = locationId;
editLocationModal.querySelector('#edit_name').value = locationName;
});
}
});
</script>
<?php require_once 'footer.php'; ?>

BIN
uploads/6852fc1190bc2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
uploads/685306028b828.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

BIN
uploads/685306a5be4af.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

BIN
uploads/68530b5956db3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

BIN
uploads/685314e648206.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

BIN
uploads/6853157b136ce.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

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