diff --git a/README.md b/README.md index f7fd944..f6f4bcc 100644 --- a/README.md +++ b/README.md @@ -180,3 +180,7 @@ Das Projekt basiert auf bewährten Web-Standards: * **Details:** * Design-Update: Diagramme verwenden nun kontrastreiche Grüntöne passend zum Thema (ohne weiße Rahmen). * Statistik: Klickbare Trägernamen öffnen ein Modal mit detaillierten Gewichtsstatistiken pro Kategorie. +* **Modulare Rucksack-Erweiterung:** + * Fächer können nun direkt mit Artikeln verknüpft werden (z.B. "Hüftgurttasche"). + * Diese "Zusatztaschen" werden in der Packliste automatisch als Container angelegt. + * Das Gewicht der Zusatztaschen wird automatisch zum Rucksack-Gesamtgewicht addiert. diff --git a/src/backpacks.php b/src/backpacks.php index be2f0ac..a58463c 100644 --- a/src/backpacks.php +++ b/src/backpacks.php @@ -48,7 +48,11 @@ $backpacks = []; $check_col = $conn->query("SHOW COLUMNS FROM backpacks LIKE 'product_url'"); $has_product_url = ($check_col && $check_col->num_rows > 0); -$sql = "SELECT b.*, u.username as owner_name +$sql = "SELECT b.*, u.username as owner_name, + (SELECT SUM(a.weight_grams) + FROM backpack_compartments bc + JOIN articles a ON bc.linked_article_id = a.id + WHERE bc.backpack_id = b.id) as extra_weight FROM backpacks b JOIN users u ON b.user_id = u.id WHERE b.user_id = ?"; @@ -85,7 +89,9 @@ while ($row = $result->fetch_assoc()) {
- +
@@ -109,7 +115,13 @@ while ($row = $result->fetch_assoc()) {

- g + + + g + 0): ?> + (+) + + L
diff --git a/src/edit_backpack.php b/src/edit_backpack.php index d7553e6..5d17b08 100644 --- a/src/edit_backpack.php +++ b/src/edit_backpack.php @@ -25,11 +25,16 @@ $message = ''; $image_url = ''; $product_url = ''; -// AUTO-MIGRATION: Check if product_url column exists, if not add it +// AUTO-MIGRATION: Check columns $check_col = $conn->query("SHOW COLUMNS FROM backpacks LIKE 'product_url'"); if ($check_col && $check_col->num_rows == 0) { $conn->query("ALTER TABLE backpacks ADD COLUMN product_url VARCHAR(255) DEFAULT NULL AFTER image_url"); } +$check_col_c = $conn->query("SHOW COLUMNS FROM backpack_compartments LIKE 'linked_article_id'"); +if ($check_col_c && $check_col_c->num_rows == 0) { + $conn->query("ALTER TABLE backpack_compartments ADD COLUMN linked_article_id INT DEFAULT NULL"); + $conn->query("ALTER TABLE backpack_compartments ADD CONSTRAINT fk_bc_article FOREIGN KEY (linked_article_id) REFERENCES articles(id) ON DELETE SET NULL"); +} // Check Household $household_id = null; @@ -105,7 +110,29 @@ $stmt_man_load->execute(); $manufacturers = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC); $stmt_man_load->close(); -// Handle Form Submission BEFORE loading header +// Load Articles for Linked Compartments +// Filter: Show all articles (or maybe only containers/bags? User said "any"). Let's load all. +$hh_ids = [$user_id]; +if ($household_id) { + // Get all users in household + $stmt_hhm = $conn->prepare("SELECT id FROM users WHERE household_id = ?"); + $stmt_hhm->bind_param("i", $household_id); + $stmt_hhm->execute(); + $res_hhm = $stmt_hhm->get_result(); + while($r = $res_hhm->fetch_assoc()) $hh_ids[] = $r['id']; +} +$placeholders = implode(',', array_fill(0, count($hh_ids), '?')); +$types_hh = str_repeat('i', count($hh_ids)); +$stmt_arts = $conn->prepare("SELECT id, name, weight_grams, image_url FROM articles WHERE user_id IN ($placeholders) OR household_id = ? ORDER BY name ASC"); +$params_arts = array_merge($hh_ids, [$household_id ?: 0]); // 0 if null +$types_arts = $types_hh . 'i'; +$stmt_arts->bind_param($types_arts, ...$params_arts); +$stmt_arts->execute(); +$all_articles = $stmt_arts->get_result()->fetch_all(MYSQLI_ASSOC); +$stmt_arts->close(); + + +// Handle Form Submission if ($_SERVER['REQUEST_METHOD'] == 'POST') { $name = trim($_POST['name']); @@ -115,7 +142,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($_POST['manufacturer_select'] === 'new') { $new_man = trim($_POST['new_manufacturer_name']); if (!empty($new_man)) { - // Check if exists first $stmt_check_man = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id = ? AND name = ?"); $stmt_check_man->bind_param("is", $user_id, $new_man); $stmt_check_man->execute(); @@ -124,7 +150,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { if ($res_check->num_rows > 0) { $manufacturer = $res_check->fetch_assoc()['name']; } else { - // Save to manufacturers table $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(); @@ -132,7 +157,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { } } } else { - // Look up name from ID $man_id = intval($_POST['manufacturer_select']); foreach ($manufacturers as $m) { if ($m['id'] == $man_id) { @@ -150,52 +174,25 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { $product_url_input = trim($_POST['product_url'] ?? ''); // Image Handling - $image_url_for_db = $image_url; // Keep existing by default + $image_url_for_db = $image_url; $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 .= '
Fehler beim Speichern des eingefügten Bildes: ' . htmlspecialchars($res) . '
'; - } - } 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 .= '
Fehler beim Verschieben der Datei. Schreibrechte prüfen.
'; - } - } else { - $message .= '
Ungültiges Dateiformat. Erlaubt: JPG, PNG, GIF, WEBP.
'; + if ($ok) $image_url_for_db = $res; + } elseif (isset($_FILES['image_file']) && $_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 { - // 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 .= '
Upload-Fehler: ' . $err_msg . '
'; } } elseif (!empty($url_image)) { list($ok, $res) = save_image_from_url($url_image, $upload_dir); - if ($ok) { - $image_url_for_db = $res; - } else { - $message .= '
Fehler beim Laden von URL: ' . htmlspecialchars($res) . '
'; - } + if ($ok) $image_url_for_db = $res; } $final_household_id = ($share_household && $household_id) ? $household_id : NULL; @@ -214,11 +211,14 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { } // Handle Compartments - if (isset($_POST['compartment_names'])) { - $comp_names = $_POST['compartment_names']; - $comp_ids = $_POST['compartment_ids'] ?? []; + // Arrays: comp_ids, comp_types (text/article), comp_names, comp_articles + if (isset($_POST['comp_types'])) { + $types = $_POST['comp_types']; + $ids = $_POST['comp_ids'] ?? []; + $names = $_POST['comp_names'] ?? []; + $articles = $_POST['comp_articles'] ?? []; - // Get existing IDs to know what to delete + // Get existing IDs $existing_ids = []; if($backpack_id > 0){ $stmt_check = $conn->prepare("SELECT id FROM backpack_compartments WHERE backpack_id = ?"); @@ -230,22 +230,37 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { $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; + for ($i = 0; $i < count($types); $i++) { + $c_id = intval($ids[$i] ?? 0); + $c_type = $types[$i]; + $c_name = ''; + $c_article_id = NULL; + + if ($c_type === 'article') { + $c_article_id = intval($articles[$i] ?? 0); + if ($c_article_id <= 0) continue; // Skip invalid + // Get name from article for display purposes (fallback) + foreach($all_articles as $art) { + if ($art['id'] == $c_article_id) { + $c_name = $art['name']; + break; + } + } + } else { + $c_name = trim($names[$i] ?? ''); + 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 = $conn->prepare("UPDATE backpack_compartments SET name = ?, sort_order = ?, linked_article_id = ? WHERE id = ?"); + $stmt_up->bind_param("siii", $c_name, $i, $c_article_id, $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 = $conn->prepare("INSERT INTO backpack_compartments (backpack_id, name, sort_order, linked_article_id) VALUES (?, ?, ?, ?)"); + $stmt_in->bind_param("isii", $backpack_id, $c_name, $i, $c_article_id); $stmt_in->execute(); } } @@ -311,8 +326,9 @@ require_once 'header.php';
- - + + +
Ohne Zusatztaschen.
@@ -346,43 +362,75 @@ require_once 'header.php';
-
Fächeraufteilung
+
Fächer & Zusatztaschen
- Definiere hier die Bereiche deines Rucksacks (z.B. Deckelfach, Bodenfach). Diese erscheinen später in der Packliste als Container. + Definiere hier die Bereiche deines Rucksacks. Du kannst einfache Text-Fächer (z.B. "Deckelfach") oder echte Artikel als Zusatztaschen (z.B. "Hüftgurttasche") hinzufügen. Das Gewicht von Zusatztaschen wird zum Rucksackgewicht addiert.
- -
- - - - -
-
- - - - + +
+
+
+ + + + + + + + + + + +
+
-
- - - - +
+
+
+ + + + + +
+ +
+ + + +
+
- +
Abbrechen @@ -414,53 +462,93 @@ document.addEventListener('DOMContentLoaded', function() { } if (manSelect) { - // Initial check toggleManContainer(manSelect.value); - - // Vanilla Listener (Backup) - manSelect.addEventListener('change', function() { - toggleManContainer(this.value); - }); - } - - // Tom Select Init - if(manSelect) { - new TomSelect(manSelect, { - create: false, - sortField: { field: "text", direction: "asc" }, - onChange: function(value) { - toggleManContainer(value); - } - }); + manSelect.addEventListener('change', function() { toggleManContainer(this.value); }); + new TomSelect(manSelect, { create: false, sortField: { field: "text", direction: "asc" }, onChange: function(value) { toggleManContainer(value); } }); } const container = document.getElementById('compartments-container'); - // Sortable for Compartments new Sortable(container, { - handle: '.input-group-text', + handle: '.handle', animation: 150 }); + // Template for new row (with placeholders) + // Note: We use a class 'tom-select-init' to mark selects that need init + const rowTemplate = ` +
+
+
+ + + + + + + + + +
+
+
+ `; + document.getElementById('add-compartment').addEventListener('click', function() { - const div = document.createElement('div'); - div.className = 'input-group mb-2 compartment-row'; - div.innerHTML = - ` - - - - `; - container.appendChild(div); + container.insertAdjacentHTML('beforeend', rowTemplate); + // Init new elements + const newRow = container.lastElementChild; + initRowLogic(newRow); }); + function initRowLogic(row) { + const typeSelect = row.querySelector('.type-select'); + const nameInput = row.querySelector('.name-input'); + const artWrapper = row.querySelector('.article-select-wrapper'); + const artSelect = row.querySelector('.article-select'); + + // Type Toggle Logic + typeSelect.addEventListener('change', function() { + if (this.value === 'article') { + nameInput.style.display = 'none'; + artWrapper.style.display = 'block'; + // Init TomSelect if not yet done + if (artSelect.classList.contains('tom-select-init') && !artSelect.tomselect) { + new TomSelect(artSelect, { create: false, sortField: { field: "text", direction: "asc" } }); + artSelect.classList.remove('tom-select-init'); + } + } else { + nameInput.style.display = 'block'; + artWrapper.style.display = 'none'; + } + }); + + // Initialize TomSelect for existing items (if visible) or prepare them + if (artSelect && !artSelect.tomselect) { + new TomSelect(artSelect, { create: false, sortField: { field: "text", direction: "asc" } }); + } + } + + // Init existing rows + container.querySelectorAll('.compartment-row').forEach(initRowLogic); + container.addEventListener('click', function(e) { if (e.target.closest('.btn-remove-comp')) { e.target.closest('.compartment-row').remove(); } }); - // Image Handling Logic + // Image Handling Logic (same as before) const imageFileInput = document.getElementById('image_file'); const imagePreview = document.getElementById('imagePreview'); const pasteArea = document.getElementById('pasteArea'); @@ -500,4 +588,4 @@ document.addEventListener('DOMContentLoaded', function() { }); - + \ No newline at end of file