Refactor: Moved app source code back to 'src/' directory for cleaner Docker build context
373
src/add_article.php
Normal file
@@ -0,0 +1,373 @@
|
||||
<?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();
|
||||
?>
|
||||
|
||||
<!-- Tom Select CSS/JS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.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-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;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Tom Select for searchable dropdowns
|
||||
const tsOptions = {
|
||||
create: false,
|
||||
sortField: { field: "text", direction: "asc" },
|
||||
onChange: function(value) {
|
||||
// Force trigger change event for the "New" option toggle logic
|
||||
const event = new Event('change');
|
||||
this.input.dispatchEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
if(document.getElementById('manufacturer_id')) new TomSelect('#manufacturer_id', tsOptions);
|
||||
if(document.getElementById('category_id')) new TomSelect('#category_id', tsOptions);
|
||||
if(document.getElementById('parent_article_id')) new TomSelect('#parent_article_id', tsOptions);
|
||||
if(document.getElementById('storage_location_id')) new TomSelect('#storage_location_id', tsOptions);
|
||||
});
|
||||
</script>
|
||||
<?php require_once 'footer.php'; ?>
|
||||
192
src/add_packing_list.php
Normal 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
Normal 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
Normal 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">∞</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'; ?>
|
||||
967
src/assets/css/style.css
Executable file
@@ -0,0 +1,967 @@
|
||||
: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: visible; /* Allow dropdowns to overflow */
|
||||
background-color: var(--glass-bg);
|
||||
backdrop-filter: var(--glass-blur);
|
||||
position: relative;
|
||||
transition: z-index 0.1s step-start; /* Instant z-index change */
|
||||
}
|
||||
|
||||
/* Lift card when interacting to ensure dropdowns appear over subsequent cards */
|
||||
.section-card:hover, .section-card:focus-within {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.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;
|
||||
border-top-left-radius: var(--border-radius-md);
|
||||
border-top-right-radius: var(--border-radius-md);
|
||||
}
|
||||
|
||||
.section-card .card-body {
|
||||
background-color: transparent;
|
||||
overflow: visible; /* Allow dropdowns to overflow */
|
||||
}
|
||||
|
||||
.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
@@ -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, image_url 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;
|
||||
}
|
||||
?>
|
||||
143
src/backpacks.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?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>
|
||||
|
||||
<?php if(!empty($bp['image_url'])): ?>
|
||||
<div class="mb-3 text-center" style="height: 150px; overflow: hidden; background-color: #f8f9fa; border-radius: 5px; display: flex; align-items: center; justify-content: center;">
|
||||
<img src="<?php echo htmlspecialchars($bp['image_url']); ?>" alt="Rucksackbild" style="max-width: 100%; max-height: 100%; object-fit: contain;">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<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
Normal 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'; ?>
|
||||
42
src/db_connect.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// db_connect.php - Zentrale Datei für die Datenbankverbindung
|
||||
|
||||
// 1. Priorität: Umgebungsvariablen (Docker / Environment)
|
||||
$servername = getenv('DB_HOST');
|
||||
$username = getenv('DB_USER');
|
||||
$password = getenv('DB_PASSWORD');
|
||||
$dbname = getenv('DB_NAME');
|
||||
|
||||
// 2. Fallback: config.ini (Lokale Entwicklung)
|
||||
if (!$servername || !$username || !$dbname) {
|
||||
$config_path = __DIR__ . '/../config.ini';
|
||||
if (file_exists($config_path)) {
|
||||
$config = parse_ini_file($config_path);
|
||||
if ($config) {
|
||||
$servername = $config['servername'] ?? null;
|
||||
$username = $config['username'] ?? null;
|
||||
$password = $config['password'] ?? null;
|
||||
$dbname = $config['dbname'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$servername || !$username || !$dbname) {
|
||||
die("Kritischer Fehler: Keine Datenbank-Konfiguration gefunden (Weder ENV noch config.ini).");
|
||||
}
|
||||
|
||||
// Verbindung herstellen
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
|
||||
// Verbindung prüfen
|
||||
if ($conn->connect_error) {
|
||||
error_log("Datenbankverbindungsfehler: " . $conn->connect_error);
|
||||
die("Verbindung zur Datenbank konnte nicht hergestellt werden.");
|
||||
}
|
||||
|
||||
// Zeichensatz auf UTF-8 setzen
|
||||
$conn->set_charset("utf8mb4");
|
||||
|
||||
// Zeitzone setzen
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
?>
|
||||
69
src/delete_article.php
Normal 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
Normal 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
Normal 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;
|
||||
?>
|
||||
446
src/edit_article.php
Normal file
@@ -0,0 +1,446 @@
|
||||
<?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();
|
||||
?>
|
||||
|
||||
<!-- Tom Select CSS/JS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.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>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;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Tom Select
|
||||
const tsOptions = {
|
||||
create: false,
|
||||
sortField: { field: "text", direction: "asc" },
|
||||
onChange: function(value) {
|
||||
const event = new Event('change');
|
||||
this.input.dispatchEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
if(document.getElementById('manufacturer_id')) new TomSelect('#manufacturer_id', tsOptions);
|
||||
if(document.getElementById('category_id')) new TomSelect('#category_id', tsOptions);
|
||||
if(document.getElementById('parent_article_id')) new TomSelect('#parent_article_id', tsOptions);
|
||||
if(document.getElementById('storage_location_id')) new TomSelect('#storage_location_id', tsOptions);
|
||||
});
|
||||
</script>
|
||||
<?php require_once 'footer.php'; ?>
|
||||
474
src/edit_backpack.php
Normal file
@@ -0,0 +1,474 @@
|
||||
<?php
|
||||
// Check for POST size limit exceeded immediately
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST' && empty($_POST) && empty($_FILES) && $_SERVER['CONTENT_LENGTH'] > 0) {
|
||||
$max_size = ini_get('post_max_size');
|
||||
die("Fehler: Die hochgeladene Datei überschreitet das Server-Limit von $max_size. Bitte wählen Sie ein kleineres Bild.");
|
||||
}
|
||||
|
||||
// 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 = '';
|
||||
$image_url = '';
|
||||
|
||||
// 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'];
|
||||
}
|
||||
|
||||
// Image Upload Config
|
||||
$upload_dir = 'uploads/images/';
|
||||
if (!is_dir($upload_dir)) { @mkdir($upload_dir, 0777, true); }
|
||||
|
||||
function save_image_from_url($url, $upload_dir) {
|
||||
$context_options = ["http" => ["header" => "User-Agent: Mozilla/5.0\r\n", "timeout" => 10]];
|
||||
$context = stream_context_create($context_options);
|
||||
$image_data = @file_get_contents($url, false, $context);
|
||||
if ($image_data === false) return [false, "Bild-Download fehlgeschlagen."];
|
||||
$image_info = @getimagesizefromstring($image_data);
|
||||
if ($image_info === false) return [false, "Ungültiges Bild."];
|
||||
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!in_array($image_info['mime'], $allowed)) return [false, "Format nicht unterstützt."];
|
||||
$ext = image_type_to_extension($image_info[2], false);
|
||||
$name = uniqid('bp_url_', true) . '.' . $ext;
|
||||
if (file_put_contents($upload_dir . $name, $image_data)) return [true, $upload_dir . $name];
|
||||
return [false, "Speicherfehler."];
|
||||
}
|
||||
|
||||
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, "Format nicht unterstützt."];
|
||||
$data = base64_decode($data);
|
||||
if ($data === false) return [false, "Dekodierfehler."];
|
||||
$name = uniqid('bp_paste_', true) . '.' . $type;
|
||||
if (file_put_contents($upload_dir . $name, $data)) return [true, $upload_dir . $name];
|
||||
}
|
||||
return [false, "Ungültiges Base64."];
|
||||
}
|
||||
|
||||
// Load existing data
|
||||
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();
|
||||
$image_url = $backpack['image_url'];
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Load Manufacturers
|
||||
$stmt_man_load = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id = ? ORDER BY name ASC");
|
||||
$stmt_man_load->bind_param("i", $user_id);
|
||||
$stmt_man_load->execute();
|
||||
$manufacturers = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
$stmt_man_load->close();
|
||||
|
||||
// Handle Form Submission BEFORE loading header
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$name = trim($_POST['name']);
|
||||
|
||||
// Manufacturer Logic
|
||||
$manufacturer = '';
|
||||
if (isset($_POST['manufacturer_select'])) {
|
||||
if ($_POST['manufacturer_select'] === 'new') {
|
||||
$new_man = trim($_POST['new_manufacturer_name']);
|
||||
if (!empty($new_man)) {
|
||||
// Optional: Save to manufacturers table for future use
|
||||
$stmt_new_man = $conn->prepare("INSERT INTO manufacturers (name, user_id) VALUES (?, ?)");
|
||||
$stmt_new_man->bind_param("si", $new_man, $user_id);
|
||||
$stmt_new_man->execute();
|
||||
$manufacturer = $new_man;
|
||||
}
|
||||
} else {
|
||||
// Look up name from ID
|
||||
$man_id = intval($_POST['manufacturer_select']);
|
||||
foreach ($manufacturers as $m) {
|
||||
if ($m['id'] == $man_id) {
|
||||
$manufacturer = $m['name'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$model = trim($_POST['model']);
|
||||
$weight = intval($_POST['weight_grams']);
|
||||
$volume = intval($_POST['volume_liters']);
|
||||
$share_household = isset($_POST['share_household']) ? 1 : 0;
|
||||
|
||||
// Image Handling
|
||||
$image_url_for_db = $image_url; // Keep existing by default
|
||||
$pasted_image = $_POST['pasted_image_data'] ?? '';
|
||||
$url_image = trim($_POST['image_url_input'] ?? '');
|
||||
|
||||
if (!empty($pasted_image)) {
|
||||
list($ok, $res) = save_image_from_base64($pasted_image, $upload_dir);
|
||||
if ($ok) {
|
||||
$image_url_for_db = $res;
|
||||
} else {
|
||||
$message .= '<div class="alert alert-warning">Fehler beim Speichern des eingefügten Bildes: ' . htmlspecialchars($res) . '</div>';
|
||||
}
|
||||
} elseif (isset($_FILES['image_file']) && $_FILES['image_file']['error'] != UPLOAD_ERR_NO_FILE) {
|
||||
// User attempted to upload a file
|
||||
if ($_FILES['image_file']['error'] == UPLOAD_ERR_OK) {
|
||||
$ext = strtolower(pathinfo($_FILES['image_file']['name'], PATHINFO_EXTENSION));
|
||||
$allowed_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
if (in_array($ext, $allowed_exts)) {
|
||||
$name_file = uniqid('bp_img_', true) . '.' . $ext;
|
||||
if (move_uploaded_file($_FILES['image_file']['tmp_name'], $upload_dir . $name_file)) {
|
||||
$image_url_for_db = $upload_dir . $name_file;
|
||||
} else {
|
||||
$message .= '<div class="alert alert-danger">Fehler beim Verschieben der Datei. Schreibrechte prüfen.</div>';
|
||||
}
|
||||
} else {
|
||||
$message .= '<div class="alert alert-warning">Ungültiges Dateiformat. Erlaubt: JPG, PNG, GIF, WEBP.</div>';
|
||||
}
|
||||
} else {
|
||||
// Handle upload errors
|
||||
$err_code = $_FILES['image_file']['error'];
|
||||
$err_msg = 'Unbekannter Fehler';
|
||||
switch ($err_code) {
|
||||
case UPLOAD_ERR_INI_SIZE: $err_msg = 'Datei ist zu groß (php.ini limit).'; break;
|
||||
case UPLOAD_ERR_FORM_SIZE: $err_msg = 'Datei ist zu groß (HTML form limit).'; break;
|
||||
case UPLOAD_ERR_PARTIAL: $err_msg = 'Datei wurde nur teilweise hochgeladen.'; break;
|
||||
case UPLOAD_ERR_NO_TMP_DIR: $err_msg = 'Kein temporärer Ordner gefunden.'; break;
|
||||
case UPLOAD_ERR_CANT_WRITE: $err_msg = 'Fehler beim Schreiben auf die Festplatte.'; break;
|
||||
}
|
||||
$message .= '<div class="alert alert-danger">Upload-Fehler: ' . $err_msg . '</div>';
|
||||
}
|
||||
} elseif (!empty($url_image)) {
|
||||
list($ok, $res) = save_image_from_url($url_image, $upload_dir);
|
||||
if ($ok) {
|
||||
$image_url_for_db = $res;
|
||||
} else {
|
||||
$message .= '<div class="alert alert-warning">Fehler beim Laden von URL: ' . htmlspecialchars($res) . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
$final_household_id = ($share_household && $household_id) ? $household_id : NULL;
|
||||
|
||||
if ($backpack_id > 0) {
|
||||
// Update
|
||||
$stmt = $conn->prepare("UPDATE backpacks SET name=?, manufacturer=?, model=?, weight_grams=?, volume_liters=?, household_id=?, image_url=? WHERE id=? AND user_id=?");
|
||||
$stmt->bind_param("sssiissii", $name, $manufacturer, $model, $weight, $volume, $final_household_id, $image_url_for_db, $backpack_id, $user_id);
|
||||
$stmt->execute();
|
||||
} else {
|
||||
// Insert
|
||||
$stmt = $conn->prepare("INSERT INTO backpacks (user_id, household_id, name, manufacturer, model, weight_grams, volume_liters, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param("iisssiis", $user_id, $final_household_id, $name, $manufacturer, $model, $weight, $volume, $image_url_for_db);
|
||||
$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
|
||||
$existing_ids = [];
|
||||
if($backpack_id > 0){
|
||||
$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;
|
||||
}
|
||||
|
||||
// Handle Form Submission BEFORE loading header
|
||||
// ... (existing logic) ...
|
||||
|
||||
require_once 'header.php';
|
||||
?>
|
||||
|
||||
<!-- Tom Select CSS/JS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
|
||||
|
||||
<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" enctype="multipart/form-data">
|
||||
<input type="hidden" name="pasted_image_data" id="pasted_image_data">
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="row g-3">
|
||||
<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>
|
||||
<select class="form-select" id="manufacturer_select" name="manufacturer_select">
|
||||
<option value="">-- Auswählen --</option>
|
||||
<option value="new">-- Neuen anlegen --</option>
|
||||
<?php
|
||||
$current_man = $backpack['manufacturer'] ?? '';
|
||||
foreach ($manufacturers as $man):
|
||||
$selected = ($man['name'] === $current_man) ? 'selected' : '';
|
||||
?>
|
||||
<option value="<?php echo $man['id']; ?>" <?php 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" name="new_manufacturer_name" class="form-control form-control-sm" placeholder="Name des neuen Herstellers">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">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>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Bild</label>
|
||||
<div class="card bg-light mb-2">
|
||||
<div class="card-body text-center p-2">
|
||||
<img id="imagePreview" src="<?php echo !empty($image_url) ? htmlspecialchars($image_url) : 'keinbild.png'; ?>" class="img-fluid" style="max-height:150px; object-fit:contain;">
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" name="image_file" id="image_file" class="form-control form-control-sm mb-2" accept="image/*">
|
||||
<input type="text" name="image_url_input" id="image_url_input" class="form-control form-control-sm mb-2" placeholder="oder Bild-URL">
|
||||
<div id="pasteArea" class="paste-area small py-2" style="border: 2px dashed #ccc; text-align:center; cursor:pointer;">Bild einfügen (Strg+V)</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):
|
||||
// Ensure proper escaping for HTML attributes
|
||||
$comp_name_escaped = htmlspecialchars($comp['name']);
|
||||
$comp_id_escaped = htmlspecialchars($comp['id']);
|
||||
?>
|
||||
<div class="input-group mb-2 compartment-row">
|
||||
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
|
||||
<input type="text" name="compartment_names[]" class="form-control" value="<?php echo $comp_name_escaped; ?>">
|
||||
<input type="hidden" name="compartment_ids[]" value="<?php echo $comp_id_escaped; ?>">
|
||||
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<?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 manSelect = document.getElementById('manufacturer_select');
|
||||
const manContainer = document.getElementById('new_manufacturer_container');
|
||||
|
||||
if (manSelect) {
|
||||
manSelect.addEventListener('change', function() {
|
||||
if (this.value === 'new') {
|
||||
manContainer.style.display = 'block';
|
||||
manContainer.querySelector('input').required = true;
|
||||
} else {
|
||||
manContainer.style.display = 'none';
|
||||
manContainer.querySelector('input').required = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
// Image Handling Logic (copied from add_article.php pattern)
|
||||
const imageFileInput = document.getElementById('image_file');
|
||||
const imagePreview = document.getElementById('imagePreview');
|
||||
const pasteArea = document.getElementById('pasteArea');
|
||||
const pastedImageDataInput = document.getElementById('pasted_image_data');
|
||||
|
||||
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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pasteArea) {
|
||||
// ... existing paste logic ...
|
||||
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;
|
||||
pasteArea.innerText = "Bild eingefügt!";
|
||||
pasteArea.style.borderColor = "green";
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Add focus to paste area on click so it can receive paste events
|
||||
pasteArea.setAttribute('tabindex', '0');
|
||||
pasteArea.addEventListener('click', () => pasteArea.focus());
|
||||
}
|
||||
|
||||
// Tom Select Init
|
||||
const manSelect = document.getElementById('manufacturer_select');
|
||||
if(manSelect) {
|
||||
new TomSelect(manSelect, {
|
||||
create: false,
|
||||
sortField: { field: "text", direction: "asc" },
|
||||
onChange: function(value) {
|
||||
const event = new Event('change');
|
||||
manSelect.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once 'footer.php'; ?>
|
||||
368
src/edit_packing_list_details.php
Normal file
@@ -0,0 +1,368 @@
|
||||
<?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
|
||||
$all_assigned_backpack_ids = array_values($current_assignments);
|
||||
|
||||
// Prepare data for JS
|
||||
$user_backpacks_json = [];
|
||||
|
||||
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;
|
||||
|
||||
// Store for JS
|
||||
$user_backpacks_json[$user['id']] = [
|
||||
'current_id' => $my_current_bp_id,
|
||||
'backpacks' => $user_backpacks
|
||||
];
|
||||
|
||||
// Find current backpack details for display
|
||||
$current_bp_details = null;
|
||||
foreach ($user_backpacks as $bp) {
|
||||
if ($bp['id'] == $my_current_bp_id) {
|
||||
$current_bp_details = $bp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="mb-4 border-bottom pb-3">
|
||||
<label class="form-label fw-bold mb-2"><?php echo htmlspecialchars($user['username']); ?></label>
|
||||
<input type="hidden" name="backpacks[<?php echo $user['id']; ?>]" id="input_bp_<?php echo $user['id']; ?>" value="<?php echo $my_current_bp_id; ?>">
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- Selected Backpack Display -->
|
||||
<div id="display_bp_<?php echo $user['id']; ?>" class="flex-grow-1 p-2 border rounded bg-white d-flex align-items-center" style="min-height: 60px;">
|
||||
<?php if ($current_bp_details): ?>
|
||||
<!-- Image if available -->
|
||||
<?php if(!empty($current_bp_details['image_url'])): ?>
|
||||
<img src="<?php echo htmlspecialchars($current_bp_details['image_url']); ?>" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" class="me-2">
|
||||
<?php else: ?>
|
||||
<i class="fas fa-hiking fa-2x text-muted me-2"></i>
|
||||
<?php endif; ?>
|
||||
<div>
|
||||
<div class="fw-bold small"><?php echo htmlspecialchars($current_bp_details['name']); ?></div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small fst-italic">Kein Rucksack zugewiesen</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-2" onclick="openBackpackModal(<?php echo $user['id']; ?>, '<?php echo htmlspecialchars($user['username']); ?>')">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Generic Modal -->
|
||||
<div class="modal fade" id="genericBpModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="genericBpModalTitle">Rucksack wählen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body bg-light" id="genericBpModalBody">
|
||||
<!-- Content loaded via JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const userBackpacksData = <?php echo json_encode($user_backpacks_json); ?>;
|
||||
const allAssignedIds = <?php echo json_encode($all_assigned_backpack_ids); ?>;
|
||||
|
||||
function openBackpackModal(userId, username) {
|
||||
const data = userBackpacksData[userId];
|
||||
const currentBpId = data.current_id;
|
||||
const titleEl = document.getElementById('genericBpModalTitle');
|
||||
const bodyEl = document.getElementById('genericBpModalBody');
|
||||
|
||||
titleEl.textContent = 'Rucksack für ' + username + ' wählen';
|
||||
|
||||
let html = '<div class="row g-3">';
|
||||
|
||||
// Option: None
|
||||
html += `
|
||||
<div class="col-md-4 col-6">
|
||||
<div class="card h-100 text-center p-3 bp-select-card ${currentBpId == 0 ? 'border-primary bg-white' : ''}" onclick="selectBackpack(${userId}, 0)">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-times-circle fa-2x text-muted mb-2"></i>
|
||||
<div class="small fw-bold">Kein Rucksack</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
data.backpacks.forEach(bp => {
|
||||
// Filter logic
|
||||
const isAssigned = allAssignedIds.some(id => String(id) === String(bp.id));
|
||||
if (isAssigned && String(bp.id) !== String(currentBpId)) return;
|
||||
|
||||
const imgHtml = bp.image_url ? `<img src="${bp.image_url}" class="card-img-top" style="height: 100px; object-fit: contain; padding: 10px;">` : `<div class="text-center py-4"><i class="fas fa-hiking fa-3x text-muted"></i></div>`;
|
||||
const activeClass = (String(bp.id) === String(currentBpId)) ? 'border-primary bg-white' : '';
|
||||
|
||||
html += `
|
||||
<div class="col-md-4 col-6">
|
||||
<div class="card h-100 bp-select-card ${activeClass}" onclick='selectBackpack(${userId}, ${bp.id})'>
|
||||
${imgHtml}
|
||||
<div class="card-body p-2 text-center">
|
||||
<div class="small fw-bold text-truncate">${bp.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
bodyEl.innerHTML = html;
|
||||
|
||||
new bootstrap.Modal(document.getElementById('genericBpModal')).show();
|
||||
}
|
||||
|
||||
function selectBackpack(userId, bpId) {
|
||||
// Update Hidden Input
|
||||
document.getElementById('input_bp_' + userId).value = bpId;
|
||||
|
||||
// Find BP Data for display
|
||||
let bpName = 'Kein Rucksack zugewiesen';
|
||||
let bpImage = null;
|
||||
|
||||
if (bpId > 0) {
|
||||
const data = userBackpacksData[userId];
|
||||
const bp = data.backpacks.find(b => String(b.id) === String(bpId));
|
||||
if (bp) {
|
||||
bpName = bp.name;
|
||||
bpImage = bp.image_url;
|
||||
}
|
||||
}
|
||||
|
||||
// Update Display
|
||||
const displayDiv = document.getElementById('display_bp_' + userId);
|
||||
let html = '';
|
||||
if (bpId == 0) {
|
||||
html = '<span class="text-muted small fst-italic">Kein Rucksack zugewiesen</span>';
|
||||
} else {
|
||||
if (bpImage) {
|
||||
html += '<img src="' + bpImage + '" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" class="me-2">';
|
||||
} else {
|
||||
html += '<i class="fas fa-hiking fa-2x text-muted me-2"></i>';
|
||||
}
|
||||
html += '<div><div class="fw-bold small">' + bpName + '</div></div>';
|
||||
}
|
||||
displayDiv.innerHTML = html;
|
||||
|
||||
// Close Modal
|
||||
const modalEl = document.getElementById('genericBpModal');
|
||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||
modal.hide();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bp-select-card { cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; border: 1px solid rgba(0,0,0,0.1); }
|
||||
.bp-select-card:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); border-color: var(--color-primary); }
|
||||
</style>
|
||||
|
||||
<?php require_once 'footer.php'; ?>
|
||||
51
src/footer.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
// footer.php - Globale Footer-Struktur
|
||||
?>
|
||||
<footer class="main-footer">
|
||||
© <?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
Normal 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
Normal 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
Normal 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
Normal 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
Normal 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
Normal file
|
After Width: | Height: | Size: 24 KiB |
101
src/login.php
Normal 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
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
29
src/logout.php
Normal 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;
|
||||
?>
|
||||
592
src/manage_packing_list_items.php
Normal file
@@ -0,0 +1,592 @@
|
||||
<?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, a.image_url, 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>
|
||||
<div id="image-preview-tooltip" class="image-preview-tooltip"></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', () => {
|
||||
// Tooltip Logic
|
||||
const tooltip = document.getElementById('image-preview-tooltip');
|
||||
if (tooltip) {
|
||||
// Delegation for dynamically added items
|
||||
document.body.addEventListener('mouseover', e => {
|
||||
if (e.target.classList.contains('article-image-trigger')) {
|
||||
const url = e.target.getAttribute('data-preview-url');
|
||||
if (url && !url.endsWith('keinbild.png')) {
|
||||
tooltip.style.backgroundImage = `url('${url}')`;
|
||||
tooltip.style.display = 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('mousemove', e => {
|
||||
if (tooltip.style.display === 'block') {
|
||||
tooltip.style.left = e.pageX + 15 + 'px';
|
||||
tooltip.style.top = e.pageY + 15 + 'px';
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('mouseout', e => {
|
||||
if (e.target.classList.contains('article-image-trigger')) {
|
||||
tooltip.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
['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(' - ');
|
||||
const imgUrl = article.image_url ? article.image_url : 'keinbild.png';
|
||||
html += `
|
||||
<div class="available-item-card d-flex align-items-center" data-article-id="${article.id}">
|
||||
<img src="${imgUrl}" class="article-image-trigger me-2 rounded" style="width: 30px; height: 30px; object-fit: cover; cursor: pointer;" data-preview-url="${imgUrl}">
|
||||
<div class="flex-grow-1">
|
||||
${article.name}
|
||||
<small class="text-muted d-block" style="line-height: 1;">(${details || '---'} | ${article.weight_grams}g)</small>
|
||||
</div>
|
||||
</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
Normal 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'; ?>
|
||||
409
src/packing_list_detail.php
Normal file
@@ -0,0 +1,409 @@
|
||||
<?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>
|
||||
<a href="print_packing_list.php?id=<?php echo $packing_list_id; ?>" target="_blank" class="btn btn-outline-light btn-sm ms-2"><i class="fas fa-print me-2"></i>Drucken</a>
|
||||
</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 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
Normal 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'; ?>
|
||||
225
src/print_packing_list.php
Normal file
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
// print_packing_list.php - Eigene Seite für die Druckansicht einer 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'];
|
||||
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||
$packing_list = null;
|
||||
$items_by_parent = [];
|
||||
$message = '';
|
||||
|
||||
$total_weight_grams = 0;
|
||||
$weight_by_category = [];
|
||||
$total_consumable_weight = 0;
|
||||
|
||||
if ($packing_list_id > 0) {
|
||||
$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();
|
||||
// Permission check logic could be expanded here
|
||||
} else {
|
||||
$message = 'Packliste nicht gefunden.';
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
} else {
|
||||
$message = 'Keine ID.';
|
||||
}
|
||||
|
||||
if ($packing_list) {
|
||||
// Artikel abrufen - LEFT JOIN für Container
|
||||
// WICHTIG: LEFT JOIN users für Trägernamen
|
||||
$sql = "SELECT
|
||||
pli.id AS packing_list_item_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, bp.name, bpc.name, 'Unbekannt') AS name,
|
||||
a.weight_grams, c.name AS category_name, a.consumable, a.image_url,
|
||||
u.username AS carrier_name
|
||||
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
|
||||
LEFT JOIN backpacks bp ON pli.backpack_id = bp.id
|
||||
LEFT JOIN backpack_compartments bpc ON pli.backpack_compartment_id = bpc.id
|
||||
LEFT JOIN users u ON pli.carrier_user_id = u.id
|
||||
WHERE pli.packing_list_id = ?
|
||||
ORDER BY pli.order_index ASC";
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("i", $packing_list_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
$raw_items = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
// Fix Name for Backpacks if needed
|
||||
if ($row['backpack_id'] && empty($row['name'])) $row['name'] = "Rucksack";
|
||||
|
||||
$raw_items[] = $row;
|
||||
|
||||
// Stats
|
||||
$w = ($row['weight_grams'] ?? 0) * $row['quantity'];
|
||||
$total_weight_grams += $w;
|
||||
if ($row['consumable']) $total_consumable_weight += $w;
|
||||
$cat = $row['category_name'] ?: 'Sonstiges';
|
||||
if (!isset($weight_by_category[$cat])) $weight_by_category[$cat] = 0;
|
||||
$weight_by_category[$cat] += $w;
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
// Build Tree
|
||||
$items_by_id = [];
|
||||
foreach ($raw_items as $item) {
|
||||
$item['children'] = [];
|
||||
$items_by_id[$item['packing_list_item_id']] = $item;
|
||||
}
|
||||
foreach ($items_by_id as $id => &$item) {
|
||||
$pid = $item['parent_packing_list_item_id'];
|
||||
if ($pid && isset($items_by_id[$pid])) {
|
||||
$items_by_id[$pid]['children'][] = &$item;
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
|
||||
foreach ($items_by_id as $item) {
|
||||
if (!$item['parent_packing_list_item_id']) {
|
||||
$items_by_parent[] = $item; // Root items
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$conn->close();
|
||||
$total_weight_without_consumables = $total_weight_grams - $total_consumable_weight;
|
||||
|
||||
// Recursive Render Function for Table Rows
|
||||
function render_rows_recursive($items, $level = 0) {
|
||||
foreach ($items as $item) {
|
||||
$is_container = !empty($item['backpack_id']) || !empty($item['backpack_compartment_id']);
|
||||
$indent = $level * 20; // px
|
||||
|
||||
$row_style = "";
|
||||
$name_style = "";
|
||||
if (!empty($item['backpack_id'])) {
|
||||
$row_style = "font-weight:bold; background-color:#f0f0f0; border-top: 2px solid #000;";
|
||||
$name_style = "text-transform:uppercase;";
|
||||
} elseif (!empty($item['backpack_compartment_id'])) {
|
||||
$row_style = "font-weight:bold; background-color:#fafafa; border-bottom:1px solid #ccc;";
|
||||
$name_style = "font-style:italic;";
|
||||
}
|
||||
|
||||
echo '<tr style="' . $row_style . '">';
|
||||
|
||||
// Checkbox
|
||||
echo '<td class="print-checkbox-cell"><div class="print-checkbox" style="' . ($is_container ? 'visibility:hidden;' : '') . '"></div></td>';
|
||||
|
||||
// Name
|
||||
echo '<td style="padding-left: ' . $indent . 'px;">';
|
||||
if ($level > 0 && !$is_container) echo '<span style="color:#ccc; margin-right:5px;">↳</span>';
|
||||
echo '<span style="' . $name_style . '">' . htmlspecialchars($item['name']) . '</span>';
|
||||
echo '</td>';
|
||||
|
||||
// Qty
|
||||
echo '<td class="text-center">' . ($is_container ? '' : $item['quantity']) . '</td>';
|
||||
|
||||
// Weight
|
||||
echo '<td class="text-end">' . ($item['weight_grams'] > 0 ? number_format($item['weight_grams'], 0, ',', '.') . ' g' : '') . '</td>';
|
||||
|
||||
echo '</tr>';
|
||||
|
||||
if (!empty($item['children'])) {
|
||||
render_rows_recursive($item['children'], $level + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?php echo htmlspecialchars($packing_list['name'] ?? 'Druckansicht'); ?></title>
|
||||
<!-- Updated Version Force -->
|
||||
<style>
|
||||
@media print {
|
||||
@page { size: A4; margin: 1.5cm; }
|
||||
body { font-family: sans-serif; font-size: 10pt; -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
|
||||
.no-print { display: none; }
|
||||
}
|
||||
body { font-family: sans-serif; color: #000; max-width: 900px; margin: 0 auto; padding: 20px; }
|
||||
h1 { text-align: center; margin-bottom: 5px; }
|
||||
p.desc { text-align: center; color: #666; margin-bottom: 30px; }
|
||||
|
||||
.carrier-section { margin-bottom: 30px; page-break-inside: avoid; }
|
||||
.carrier-title { font-size: 14pt; border-bottom: 2px solid #000; margin-bottom: 10px; padding-bottom: 5px; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
|
||||
th { text-align: left; border-bottom: 1px solid #000; padding: 5px; font-size: 9pt; }
|
||||
td { padding: 4px 5px; border-bottom: 1px solid #eee; vertical-align: top; }
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.text-end { text-align: right; }
|
||||
|
||||
.print-checkbox { width: 12px; height: 12px; border: 1px solid #000; display: inline-block; }
|
||||
.print-checkbox-cell { width: 20px; }
|
||||
|
||||
.stats { margin-top: 40px; border-top: 2px solid #000; padding-top: 10px; display: flex; flex-wrap: wrap; gap: 20px; }
|
||||
.stat-item { width: 45%; margin-bottom: 5px; border-bottom: 1px dotted #ccc; display: flex; justify-content: space-between; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php if ($packing_list): ?>
|
||||
<h1><?php echo htmlspecialchars($packing_list['name']); ?></h1>
|
||||
<p class="desc"><?php echo htmlspecialchars($packing_list['description'] ?? ''); ?></p>
|
||||
|
||||
<?php
|
||||
if (empty($items_by_parent)) {
|
||||
echo '<p style="text-align:center;">Keine Artikel.</p>';
|
||||
} else {
|
||||
// Group by Carrier
|
||||
$items_by_carrier = [];
|
||||
foreach ($items_by_parent as $root) {
|
||||
$c_name = $root['carrier_name'] ?: 'Sonstiges';
|
||||
$items_by_carrier[$c_name][] = $root;
|
||||
}
|
||||
|
||||
foreach ($items_by_carrier as $carrier => $roots) {
|
||||
echo '<div class="carrier-section">';
|
||||
echo '<div class="carrier-title">' . htmlspecialchars($carrier) . '</div>';
|
||||
echo '<table>';
|
||||
echo '<thead><tr><th style="width:20px;"></th><th>Artikel / Container</th><th class="text-center" style="width:50px;">Anz.</th><th class="text-end" style="width:80px;">Gewicht</th></tr></thead>';
|
||||
echo '<tbody>';
|
||||
render_rows_recursive($roots);
|
||||
echo '</tbody>';
|
||||
echo '</table>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-item"><strong>Gesamtgewicht:</strong> <span><?php echo number_format($total_weight_grams, 0, ',', '.'); ?> g</span></div>
|
||||
<div class="stat-item"><strong>Basisgewicht (ohne Verbrauch):</strong> <span><?php echo number_format($total_weight_without_consumables, 0, ',', '.'); ?> g</span></div>
|
||||
<?php arsort($weight_by_category); foreach($weight_by_category as $cat => $w): ?>
|
||||
<div class="stat-item"><span><?php echo htmlspecialchars($cat); ?>:</span> <span><?php echo number_format($w, 0, ',', '.'); ?> g</span></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<script>window.print();</script>
|
||||
<?php else: ?>
|
||||
<p style="color:red; text-align:center;"><?php echo htmlspecialchars($message); ?></p>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
</html>
|
||||
277
src/public_list.php
Normal 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
Normal 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
Normal file
|
After Width: | Height: | Size: 22 KiB |
45
src/setup_backpacks.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once 'src/db_connect.php';
|
||||
|
||||
$sql_statements = [
|
||||
"CREATE TABLE IF NOT EXISTS backpacks (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
household_id INT DEFAULT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
manufacturer VARCHAR(255),
|
||||
model VARCHAR(255),
|
||||
weight_grams INT DEFAULT 0,
|
||||
volume_liters INT DEFAULT 0,
|
||||
image_url VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS backpack_compartments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
backpack_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
FOREIGN KEY (backpack_id) REFERENCES backpacks(id) ON DELETE CASCADE
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS packing_list_carriers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
packing_list_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
backpack_id INT DEFAULT NULL,
|
||||
FOREIGN KEY (packing_list_id) REFERENCES packing_lists(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (backpack_id) REFERENCES backpacks(id) ON DELETE SET NULL
|
||||
)"
|
||||
];
|
||||
|
||||
foreach ($sql_statements as $sql) {
|
||||
if ($conn->query($sql) === TRUE) {
|
||||
echo "Table created successfully\n";
|
||||
} else {
|
||||
echo "Error creating table: " . $conn->error . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
?>
|
||||
317
src/share_packing_list.php
Normal 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
Normal 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
|
After Width: | Height: | Size: 29 KiB |
BIN
src/uploads/685306028b828.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/685306a5be4af.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/68530b5956db3.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/685314e648206.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/6853157b136ce.png
Executable file
|
After Width: | Height: | Size: 580 KiB |
BIN
src/uploads/images/bp_img_69323e81d72cb8.63112841.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/uploads/images/bp_img_6932403035e899.09967829.jpg
Normal file
|
After Width: | Height: | Size: 902 KiB |
BIN
src/uploads/images/bp_img_6932403756cb16.99209873.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
src/uploads/images/bp_img_69324148109174.55311184.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
src/uploads/images/img_68519d40cefc91.06373491.png
Executable file
|
After Width: | Height: | Size: 236 KiB |
BIN
src/uploads/images/img_68528c880b1401.14908112.jpg
Executable file
|
After Width: | Height: | Size: 79 KiB |
BIN
src/uploads/images/img_6852aa2e9a9ac8.18122970.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_6852aae52a8590.99789219.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_6852d1707d0af4.43646571.png
Executable file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/uploads/images/img_68530888b01da3.01042792.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_685308945b37e1.08844156.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_6853093a3b0fb7.10685249.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68530974a15736.57910512.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_685309b775df63.28364476.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68530a6f2c08f5.96614673.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68530aff6faf10.53187686.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68530ba765f0c1.87888941.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68530bdb75d1a9.18204952.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68530c8fc7ce78.25162975.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68530d38ce6cd7.76753737.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68530e3263a8f3.11354541.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68530fbf34fd66.94667834.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_685310c0348007.20030955.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68531110a66323.47909040.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_685311fd9dc421.95301293.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_685312afc6c000.67281690.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_685313c68df985.82769092.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_685314c336de58.35409547.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68531517362e81.34960172.png
Executable file
|
After Width: | Height: | Size: 236 KiB |
BIN
src/uploads/images/img_685316a6098399.74821974.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68531a8befbae4.75954207.png
Executable file
|
After Width: | Height: | Size: 768 KiB |
BIN
src/uploads/images/img_68555c338e1915.37648028.png
Executable file
|
After Width: | Height: | Size: 236 KiB |
BIN
src/uploads/images/img_68555e40030f77.87225795.png
Executable file
|
After Width: | Height: | Size: 668 KiB |
BIN
src/uploads/images/img_68692cad8256a4.61815158.png
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/uploads/images/img_686eade3358e94.97676456.jpg
Executable file
|
After Width: | Height: | Size: 87 KiB |
BIN
src/uploads/images/img_686eb198525ec7.50536364.jpg
Executable file
|
After Width: | Height: | Size: 862 KiB |
BIN
src/uploads/images/img_686eb1ef18f034.45725063.png
Executable file
|
After Width: | Height: | Size: 422 KiB |
BIN
src/uploads/images/img_686eb212d64645.26126428.png
Executable file
|
After Width: | Height: | Size: 422 KiB |
BIN
src/uploads/images/img_686eb271e0c6a9.39415484.jpg
Executable file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/uploads/images/img_686eb3086df945.36369833.png
Executable file
|
After Width: | Height: | Size: 196 KiB |
BIN
src/uploads/images/img_686eb33807fd63.00035931.png
Executable file
|
After Width: | Height: | Size: 218 KiB |
BIN
src/uploads/images/img_686eb5bc838ae5.80233818.png
Executable file
|
After Width: | Height: | Size: 211 KiB |
BIN
src/uploads/images/img_686ebd1e6af955.32076521.png
Executable file
|
After Width: | Height: | Size: 188 KiB |
BIN
src/uploads/images/img_686ebdb32de4d3.21230679.jpg
Executable file
|
After Width: | Height: | Size: 54 KiB |
BIN
src/uploads/images/img_686ebe5846ea42.68414437.jpg
Executable file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/uploads/images/img_686ebeb3473cf8.68015324.jpg
Executable file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/uploads/images/img_686ebeec251778.89470573.jpg
Executable file
|
After Width: | Height: | Size: 71 KiB |
BIN
src/uploads/images/img_686ebfa1bee290.81344086.png
Executable file
|
After Width: | Height: | Size: 203 KiB |
BIN
src/uploads/images/img_686ec0042d3ad8.71473135.jpg
Executable file
|
After Width: | Height: | Size: 98 KiB |
BIN
src/uploads/images/img_686ec04d316485.12649368.png
Executable file
|
After Width: | Height: | Size: 144 KiB |
BIN
src/uploads/images/img_686ec070bc29e1.89910936.jpg
Executable file
|
After Width: | Height: | Size: 77 KiB |
BIN
src/uploads/images/img_686f30ac792db4.81124214.jpg
Executable file
|
After Width: | Height: | Size: 63 KiB |
BIN
src/uploads/images/img_686f30d181b9d3.27666927.jpg
Executable file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/uploads/images/img_686f30fc1384a1.47381841.jpg
Executable file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/uploads/images/img_686f31607f1ef4.88047468.jpg
Executable file
|
After Width: | Height: | Size: 81 KiB |
BIN
src/uploads/images/img_686f317edd6d15.06826823.jpg
Executable file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/uploads/images/img_686f31c24a1eb8.68836662.png
Executable file
|
After Width: | Height: | Size: 118 KiB |
BIN
src/uploads/images/img_686f31e47abfd0.63425175.jpg
Executable file
|
After Width: | Height: | Size: 83 KiB |