Rucksack-Feature finalisiert: Management, Zuweisung und Anzeige implementiert

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

353
src/add_article.php Executable 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 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'; ?>

192
src/add_packing_list.php Executable file
View File

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

234
src/api_packing_list_handler.php Executable file
View File

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

410
src/articles.php Executable file
View File

@@ -0,0 +1,410 @@
<?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 align-items-center">
<div class="col-md-4"><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-3"><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 class="col-md-2 text-end">
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary" id="btn-expand-all" title="Alle ausklappen"><i class="fas fa-expand-alt"></i></button>
<button type="button" class="btn btn-outline-secondary" id="btn-collapse-all" title="Alle einklappen"><i class="fas fa-compress-alt"></i></button>
</div>
</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);
document.getElementById('btn-expand-all').addEventListener('click', function() {
collapsedCategories.clear();
renderTable();
});
document.getElementById('btn-collapse-all').addEventListener('click', function() {
// Find all currently rendered categories
const visibleCategories = Array.from(document.querySelectorAll('.category-header')).map(el => el.getAttribute('data-category'));
visibleCategories.forEach(c => collapsedCategories.add(c));
renderTable();
});
renderTable();
});
</script>
<?php require_once 'footer.php'; ?>

957
src/assets/css/style.css Executable file
View File

@@ -0,0 +1,957 @@
:root {
/* Color Palette - Forest Theme */
--color-primary: #2e7d32; /* Richer Green */
--color-primary-light: #558b2f; /* Light Olive Green */
--color-primary-dark: #1b5e20;
--color-secondary: #546e7a;
--color-accent: #7cb342; /* Natural Light Green */
--color-background: #f0f2f5;
--sidebar-bg: rgba(20, 40, 20, 0.85);
/* Glassmorphism Variables - Enhanced */
--glass-bg: rgba(255, 255, 255, 0.5); /* More transparent */
--glass-border: 1px solid rgba(255, 255, 255, 0.4);
--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;
color: #1b5e20; /* Ensure Dark Green for headings */
}
/* 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: 240px; /* Reduced from 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: 15px; /* Reduced from 25px */
display: flex;
flex-direction: column;
gap: 1rem; /* Reduced from 2rem */
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: 5px;
text-align: center;
text-decoration: none;
display: block;
text-transform: uppercase;
font-size: 1rem; /* Reduced */
}
.sidebar .navbar-brand-logo {
width: 75px; /* Increased to 75px */
height: 75px;
margin-bottom: 10px;
border-radius: 20px; /* 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: 10px 15px; /* Reduced from 14px 20px */
border-radius: var(--border-radius-md);
transition: all 0.3s ease;
margin-bottom: 5px; /* Reduced */
display: flex;
align-items: center;
text-decoration: none;
font-weight: 500;
font-family: 'Poppins', sans-serif;
font-size: 0.9rem; /* Compact font */
}
.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(85, 139, 47, 0.4);
}
.sidebar .nav-link i.fa-fw {
margin-right: 15px; /* Reduced */
width: 20px;
text-align: center;
}
.sidebar .username-display {
margin-top: auto;
padding-top: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
text-align: center;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.7);
}
.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: 1.5rem; /* Reduced */
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: #1b5e20; /* Explicit Dark Green (var(--color-primary-dark)) */
border-bottom: 1px solid rgba(0,0,0,0.05);
padding: 0.5rem 1rem; /* Further reduced padding */
font-weight: 700; /* Bolder */
font-family: 'Poppins', sans-serif;
font-size: 0.95rem; /* Slightly smaller font */
}
/* 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: 1rem; /* Reduced padding */
}
/* Buttons */
.btn-primary {
background-color: var(--color-primary-light);
border-color: var(--color-primary-light);
border-radius: var(--border-radius-sm);
padding: 0.375rem 1rem; /* Reduced */
font-size: 0.9rem;
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);
}
/* Global Compact Inputs */
.form-control, .form-select {
padding: 0.375rem 0.75rem;
font-size: 0.9rem;
}
.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(85, 139, 47, 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: 220px; /* Reduced from 300px */
width: 100%;
}
/* Welcome Section (Dashboard) */
.welcome-section {
text-align: center;
padding: 20px 0; /* Reduced from 40px */
}
.welcome-section h1 {
font-size: 2rem; /* Reduced from 2.5rem */
margin-bottom: 10px;
font-weight: 700;
color: var(--color-primary);
}
.welcome-section p.lead {
font-size: 1rem; /* Reduced from 1.2rem */
color: var(--text-muted);
margin-bottom: 1rem;
}
.welcome-image-container {
max-width: 800px;
height: 180px; /* Drastically reduced from 350px */
margin: 20px auto; /* Reduced margins */
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 50px; /* Increased spacing as requested */
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;
}
/* Force compact images in tables */
.table img, .article-image-trigger {
width: 30px !important;
height: 30px !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: var(--glass-bg);
backdrop-filter: var(--glass-blur);
}
.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: transparent;
}
.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: rgba(250, 250, 250, 0.5);
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: rgba(255, 255, 255, 0.8);
}
/* --- 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: var(--glass-bg);
backdrop-filter: var(--glass-blur);
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: rgba(248, 249, 250, 0.7);
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: rgba(255, 255, 255, 0.3);
}
.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: rgba(255, 255, 255, 0.6);
border-radius: var(--border-radius-sm);
transition: background-color 0.2s;
}
.packed-item-row:hover {
background-color: rgba(255, 255, 255, 0.8);
}
.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;
background-color: var(--glass-bg);
backdrop-filter: var(--glass-blur);
}
.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: transparent;
}
.level-2-list .list-group-item {
background-color: rgba(255, 255, 255, 0.5);
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;
}
.save-feedback {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: var(--color-primary-dark);
color: #fff;
padding: 10px 20px;
border-radius: 30px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
z-index: 9999;
opacity: 0;
transition: opacity 0.3s ease-in-out;
pointer-events: none;
font-weight: 600;
font-size: 0.9rem;
}
/* --- Articles Table Styles --- */
/* Sticky Header */
.table-responsive thead th {
position: sticky;
top: 0;
background-color: var(--sidebar-bg); /* Match sidebar */
color: var(--text-light);
backdrop-filter: blur(15px);
z-index: 10; /* Above content */
box-shadow: 0 2px 2px -1px rgba(0,0,0,0.1);
}
/* Reverted Table Styling (Light) */
.table {
color: var(--text-main);
margin-bottom: 0;
}
.table th, .table td {
border-color: rgba(0, 0, 0, 0.05);
padding: 0.5rem 0.75rem; /* Compact padding */
vertical-align: middle;
}
/* Hover state */
.table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.02);
}
/* Category Header Row */
.category-header {
background-color: rgba(46, 125, 50, 0.15); /* Light subtle green */
color: var(--color-primary-dark); /* Dark text for contrast */
cursor: pointer;
font-weight: 600;
font-family: 'Poppins', sans-serif;
}
.category-header:hover {
background-color: rgba(46, 125, 50, 0.25);
}
.category-header i {
transition: transform 0.2s;
}
.category-header.collapsed i {
transform: rotate(-90deg);
}
.category-header td {
background-color: rgba(46, 125, 50, 0.15) !important;
color: var(--color-primary-dark);
border-bottom: 1px solid rgba(0,0,0,0.1);
}
/* --- 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;
}
}

73
src/backpack_utils.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
// backpack_utils.php - Shared functions for backpack management
function sync_backpack_items($conn, $list_id, $user_id, $backpack_id) {
// 1. Get Backpack Info
$bp = $conn->query("SELECT name FROM backpacks WHERE id = $backpack_id")->fetch_assoc();
// 2. Check/Create Root Item
// Use NULL safe comparison or check for NULL explicitly
// We need to allow for existing items that might be named differently if user renamed them?
// No, we stick to structure.
$root_id = 0;
$stmt = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id = ? AND backpack_id = ?");
$stmt->bind_param("iii", $list_id, $user_id, $backpack_id);
$stmt->execute();
$res = $stmt->get_result();
if ($row = $res->fetch_assoc()) {
$root_id = $row['id'];
} else {
// Create Root
$name = "Rucksack: " . $bp['name'];
// Use correct column count and NULLs
$stmt_ins = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, carrier_user_id, name, backpack_id, quantity, article_id, order_index, parent_packing_list_item_id) VALUES (?, ?, ?, ?, 1, NULL, 0, NULL)");
$stmt_ins->bind_param("iisi", $list_id, $user_id, $name, $backpack_id);
$stmt_ins->execute();
$root_id = $stmt_ins->insert_id;
}
// 3. Sync Compartments
$comps = $conn->query("SELECT id, name FROM backpack_compartments WHERE backpack_id = $backpack_id ORDER BY sort_order ASC");
while ($comp = $comps->fetch_assoc()) {
// Check if item exists for this compartment AND this user
$stmt_c = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND backpack_compartment_id = ? AND carrier_user_id = ?");
$stmt_c->bind_param("iii", $list_id, $comp['id'], $user_id);
$stmt_c->execute();
if ($stmt_c->get_result()->num_rows == 0) {
// Create Compartment Item
$c_name = $comp['name'];
$stmt_ins_c = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, carrier_user_id, name, backpack_compartment_id, parent_packing_list_item_id, quantity, article_id, order_index) VALUES (?, ?, ?, ?, ?, 1, NULL, 0)");
$stmt_ins_c->bind_param("iisii", $list_id, $user_id, $c_name, $comp['id'], $root_id);
$stmt_ins_c->execute();
}
}
}
// Helper to get backpacks for a specific user context
function get_available_backpacks_for_user($conn, $target_user_id, $household_id) {
// Check connection
if (!$conn || $conn->connect_errno) {
// Reconnect or fail gracefully?
// For now, assume caller must keep it open.
return [];
}
$bps = [];
$sql_bp = "SELECT id, name, user_id FROM backpacks WHERE user_id = ?";
if ($household_id) {
$sql_bp .= " OR household_id = ?";
$stmt_bp = $conn->prepare($sql_bp);
$stmt_bp->bind_param("ii", $target_user_id, $household_id);
} else {
$stmt_bp = $conn->prepare($sql_bp);
$stmt_bp->bind_param("i", $target_user_id);
}
$stmt_bp->execute();
$res_bp = $stmt_bp->get_result();
while ($row = $res_bp->fetch_assoc()) {
$bps[] = $row;
}
return $bps;
}
?>

137
src/backpacks.php Executable file
View File

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

171
src/categories.php Executable 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
src/db_connect.php Executable 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
src/delete_article.php Executable 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
src/delete_packing_list.php Executable 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
src/duplicate_packing_list.php Executable 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
src/edit_article.php Executable 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 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'; ?>

249
src/edit_backpack.php Executable file
View File

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

232
src/edit_packing_list_details.php Executable file
View File

@@ -0,0 +1,232 @@
<?php
// edit_packing_list_details.php - Bearbeiten von Details und Rucksack-Zuweisung
$page_title = "Packliste Details bearbeiten";
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once 'db_connect.php';
require_once 'household_actions.php';
require_once 'backpack_utils.php'; // Fix: Include utils
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$message = '';
$packing_list = null;
$can_edit = false;
// --- 1. Permissions & Basic Data ---
if ($packing_list_id > 0) {
$stmt_household_check = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_household_check->bind_param("i", $current_user_id);
$stmt_household_check->execute();
$current_user_household_id = $stmt_household_check->get_result()->fetch_assoc()['household_id'];
$stmt_household_check->close();
$stmt_list_check = $conn->prepare("SELECT id, name, description, user_id, household_id FROM packing_lists WHERE id = ?");
$stmt_list_check->bind_param("i", $packing_list_id);
$stmt_list_check->execute();
$result = $stmt_list_check->get_result();
if ($result->num_rows == 1) {
$packing_list = $result->fetch_assoc();
$is_owner = ($packing_list['user_id'] == $current_user_id);
$is_household_list = !empty($packing_list['household_id']);
$is_in_same_household = ($is_household_list && $packing_list['household_id'] == $current_user_household_id);
if ($is_owner || $is_in_same_household) {
$can_edit = true;
} else {
$message = '<div class="alert alert-danger">Keine Berechtigung.</div>';
$packing_list = null;
}
} else {
$message = '<div class="alert alert-warning">Packliste nicht gefunden.</div>';
}
} else {
$message = '<div class="alert alert-danger">Keine ID.</div>';
}
// --- 2. Fetch Data for Dropdowns ---
$available_users = [];
if ($can_edit) {
// Owners: Creator + Household Members (if shared)
if ($packing_list['household_id']) {
$stmt = $conn->prepare("SELECT id, username FROM users WHERE household_id = ?");
$stmt->bind_param("i", $packing_list['household_id']);
} else {
$stmt = $conn->prepare("SELECT id, username FROM users WHERE id = ?");
$stmt->bind_param("i", $packing_list['user_id']);
}
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
$available_users[] = $row;
}
// Current Assignments
$current_assignments = [];
$stmt_ca = $conn->prepare("SELECT user_id, backpack_id FROM packing_list_carriers WHERE packing_list_id = ?");
$stmt_ca->bind_param("i", $packing_list_id);
$stmt_ca->execute();
$res_ca = $stmt_ca->get_result();
while ($row = $res_ca->fetch_assoc()) {
$current_assignments[$row['user_id']] = $row['backpack_id'];
}
}
// --- 3. Handle Form Submission ---
if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
// Update Basic Details
$name = trim($_POST['name']);
$description = trim($_POST['description']);
$stmt_update = $conn->prepare("UPDATE packing_lists SET name = ?, description = ? WHERE id = ?");
$stmt_update->bind_param("ssi", $name, $description, $packing_list_id);
$stmt_update->execute();
$packing_list['name'] = $name;
$packing_list['description'] = $description;
// Handle Backpack Assignments
if (isset($_POST['backpacks'])) {
foreach ($_POST['backpacks'] as $uid => $bid) {
$uid = intval($uid);
$bid = intval($bid);
$bid = ($bid > 0) ? $bid : NULL;
// Update Carrier Table
// Check if exists
$old_backpack_id = 0;
$stmt_chk = $conn->prepare("SELECT id, backpack_id FROM packing_list_carriers WHERE packing_list_id = ? AND user_id = ?");
$stmt_chk->bind_param("ii", $packing_list_id, $uid);
$stmt_chk->execute();
$res_chk = $stmt_chk->get_result();
if ($row_chk = $res_chk->fetch_assoc()) {
$old_backpack_id = $row_chk['backpack_id'];
$stmt_up = $conn->prepare("UPDATE packing_list_carriers SET backpack_id = ? WHERE packing_list_id = ? AND user_id = ?");
$stmt_up->bind_param("iii", $bid, $packing_list_id, $uid);
$stmt_up->execute();
} else {
$stmt_in = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)");
$stmt_in->bind_param("iii", $packing_list_id, $uid, $bid);
$stmt_in->execute();
}
// CLEANUP LOGIC: If backpack changed or removed
if ($old_backpack_id != $bid) {
// 1. Unparent all items that are inside the old containers (so they don't get deleted)
// Find all container items for this user
$stmt_find_containers = $conn->prepare("SELECT id FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id = ? AND (backpack_id IS NOT NULL OR backpack_compartment_id IS NOT NULL)");
$stmt_find_containers->bind_param("ii", $packing_list_id, $uid);
$stmt_find_containers->execute();
$res_cont = $stmt_find_containers->get_result();
$container_ids = [];
while ($r = $res_cont->fetch_assoc()) $container_ids[] = $r['id'];
if (!empty($container_ids)) {
$ids_str = implode(',', $container_ids);
// Set parent to NULL for children of these containers
$conn->query("UPDATE packing_list_items SET parent_packing_list_item_id = NULL WHERE packing_list_id = $packing_list_id AND parent_packing_list_item_id IN ($ids_str)");
// 2. Delete the containers
$conn->query("DELETE FROM packing_list_items WHERE id IN ($ids_str)");
}
}
// SYNC LOGIC (Only if new backpack assigned)
if ($bid && $old_backpack_id != $bid) {
sync_backpack_items($conn, $packing_list_id, $uid, $bid);
}
}
}
$message = '<div class="alert alert-success">Änderungen gespeichert!</div>';
// Refresh assignments
$stmt_ca->execute();
$res_ca = $stmt_ca->get_result();
$current_assignments = [];
while ($row = $res_ca->fetch_assoc()) {
$current_assignments[$row['user_id']] = $row['backpack_id'];
}
}
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Details: <?php echo htmlspecialchars($packing_list['name'] ?? ''); ?></h2>
<a href="packing_lists.php" class="btn btn-sm btn-outline-light"><i class="fas fa-arrow-left me-2"></i>Zurück</a>
</div>
<div class="card-body p-4">
<?php echo $message; ?>
<?php if ($packing_list): ?>
<form method="post">
<div class="row">
<div class="col-md-8">
<h5 class="mb-3">Basisdaten</h5>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="name" value="<?php echo htmlspecialchars($packing_list['name']); ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Beschreibung</label>
<textarea class="form-control" name="description" rows="3"><?php echo htmlspecialchars($packing_list['description'] ?: ''); ?></textarea>
</div>
</div>
<div class="col-md-4">
<h5 class="mb-3">Rucksack-Zuweisung</h5>
<div class="card bg-light border-0">
<div class="card-body">
<p class="small text-muted">Wähle hier, wer welchen Rucksack trägt. Bereits vergebene Rucksäcke werden ausgeblendet.</p>
<?php
// Calculate used backpacks (except for the user's own current assignment)
// But we iterate users. So for each user loop, we need to know what OTHERS have assigned.
// Logic: Get all values from $current_assignments.
$all_assigned_backpack_ids = array_values($current_assignments);
foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $packing_list['household_id']);
$my_current_bp_id = $current_assignments[$user['id']] ?? 0;
?>
<div class="mb-3">
<label class="form-label fw-bold"><?php echo htmlspecialchars($user['username']); ?></label>
<select class="form-select form-select-sm" name="backpacks[<?php echo $user['id']; ?>]">
<option value="0">-- Kein Rucksack --</option>
<?php foreach ($user_backpacks as $bp):
// FILTER: If backpack is used by someone else, skip it.
// "Used by someone else" means: ID is in $all_assigned_backpack_ids AND ID != $my_current_bp_id
if (in_array($bp['id'], $all_assigned_backpack_ids) && $bp['id'] != $my_current_bp_id) {
continue;
}
?>
<option value="<?php echo $bp['id']; ?>" <?php echo ($my_current_bp_id == $bp['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($bp['name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-between">
<a href="manage_packing_list_items.php?id=<?php echo $packing_list_id; ?>" class="btn btn-info text-white"><i class="fas fa-boxes me-2"></i>Inhalt bearbeiten</a>
<button type="submit" class="btn btn-primary"><i class="fas fa-save me-2"></i>Speichern & Synchronisieren</button>
</div>
</form>
<?php endif; ?>
</div>
</div>
<?php require_once 'footer.php'; ?>

51
src/footer.php Executable 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>

104
src/header.php Executable file
View File

@@ -0,0 +1,104 @@
<?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="backpacks.php"><i class="fas fa-hiking fa-fw"></i>Rucksäcke</a></li>
<li class="nav-item"><a class="nav-link" href="packing_lists.php"><i class="fas fa-clipboard-list fa-fw"></i>Packlisten</a></li>
<li class="nav-item"><a class="nav-link" href="storage_locations.php"><i class="fas fa-archive fa-fw"></i>Lagerorte</a></li>
<li class="nav-item"><a class="nav-link" href="categories.php"><i class="fas fa-tags fa-fw"></i>Kategorien</a></li>
<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
src/help.php Executable 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
src/household.php Executable 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
src/household_actions.php Executable 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
src/index.php Executable 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
src/keinbild.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

101
src/login.php Executable 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
src/logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

29
src/logout.php Executable 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;
?>

557
src/manage_packing_list_items.php Executable file
View File

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

429
src/packing_list_detail.php Executable file
View File

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

138
src/packing_lists.php Executable 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'; ?>

283
src/print_packing_list.php Executable file
View File

@@ -0,0 +1,283 @@
<?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 - LEFT JOIN für Container Support
$stmt = $conn->prepare("SELECT pli.id AS packing_list_item_id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.backpack_id, pli.backpack_compartment_id, COALESCE(a.name, pli.name) AS name, a.weight_grams, c.name AS category_name, a.consumable, a.image_url FROM packing_list_items pli LEFT JOIN articles a ON pli.article_id = a.id LEFT JOIN categories c ON a.category_id = c.id WHERE pli.packing_list_id = ? ORDER BY pli.parent_packing_list_item_id ASC, pli.order_index ASC");
if ($stmt) {
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result();
$raw_packing_list_items = [];
while ($row = $result->fetch_assoc()) {
$raw_packing_list_items[] = $row;
$item_weight = $row['weight_grams'] ?? 0;
$item_total_weight = $item_weight * $row['quantity'];
$total_weight_grams += $item_total_weight;
$category_name = $row['category_name'] ?: 'Unkategorisiert';
if (!isset($weight_by_category[$category_name])) {
$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_weight = $item['weight_grams'] ?? 0;
$item_total_weight_current = $item_weight * $item['quantity'];
// Container Styling
$is_container = !empty($item['backpack_id']) || !empty($item['backpack_compartment_id']);
$container_style = "";
if (!empty($item['backpack_id'])) {
$container_style = "font-weight: bold; font-size: 1.1em; margin-top: 10px; border-bottom: 2px solid #ccc;";
} elseif (!empty($item['backpack_compartment_id'])) {
$container_style = "font-weight: bold; margin-top: 5px; color: #555;";
}
$details_string = '';
if (!$is_container) {
$details_string = '(' . number_format($item_weight, 0, ',', '.') . 'g' . ($item['category_name'] ? ', ' . htmlspecialchars($item['category_name']) : '') . ($item['consumable'] ? ', Verbrauchbar' : '') . ')';
}
$indent_style = ($level > 0) ? 'padding-left: ' . ($level * 1.5) . 'em;' : '';
$html .= '
<li class="print-list-item" style="' . $indent_style . ' ' . $container_style . '">
<span class="print-checkbox" style="' . ($is_container ? 'visibility:hidden;' : '') . '"></span>
<span class="print-quantity">' . ($is_container ? '' : htmlspecialchars($item['quantity']) . 'x') . '</span>
<span class="print-name">' . htmlspecialchars($item['name']) . '</span>
<span class="print-details">' . $details_string . '</span>
<span class="print-total-weight">' . ($is_container ? '' : 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
src/public_list.php Executable 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
src/register.php Executable 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
src/rucksack_icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

317
src/share_packing_list.php Executable 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
src/storage_locations.php Executable 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
src/uploads/6852fc1190bc2.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
src/uploads/685306028b828.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

BIN
src/uploads/685306a5be4af.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

BIN
src/uploads/68530b5956db3.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

BIN
src/uploads/685314e648206.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

BIN
src/uploads/6853157b136ce.png Executable 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

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