diff --git a/README.md b/README.md index 058733e..f8969cf 100644 --- a/README.md +++ b/README.md @@ -219,3 +219,12 @@ Das Projekt basiert auf bewährten Web-Standards: * Massenbearbeitung (Bulk Actions): In der Artikel-Listenansicht können nun mehrere Artikel über Checkboxen ausgewählt und gleichzeitig in einen neuen Lagerort verschoben werden. Die Auswahl bleibt auch bei Filteränderungen bestehen. * Barcode für Lagerorte: In der Lagerort-Verwaltung kann für jeden Lagerort ein QR-Code generiert und angezeigt werden. Dieser führt zu einer für Smartphones optimierten, öffentlichen Ansicht des Kisteninhalts (ohne Login nutzbar). * Packlisten-Editor (`manage_packing_list_items.php`): Ein neuer Button "Tisch leeren" ermöglicht es, alle aktuell auf dem virtuellen Tisch liegenden Artikel mit einem Klick in die Lagerorte zurückzuräumen. + +### 16.05.2026 +* **Features:** + * **Hierarchische Lagerort-Anzeige:** Lagerorte werden im Filter und beim Massenverschieben in `articles.php` nun hierarchisch mit Einrückungen (Ebene 1 und Ebene 2) dargestellt. + * **Lagerort-Filter:** Neuer Dropdown-Filter in `articles.php`, um gezielt nach Artikeln in einem bestimmten Lagerort zu suchen. + * **QR-Code Größenanpassung:** Beim Drucken von QR-Codes für Lagerorte (`storage_locations.php`) kann nun die gewünschte Größe (Höhe/Breite in mm) angegeben werden. Die gedruckten Codes haben nun zudem einen dezenten 1mm Abstand/Rahmen. + * **Strukturierte Anzeige in der Box-Ansicht:** Die öffentliche Ansicht eines Lagerorts per QR-Code (`public_location.php`) zeigt nun Artikelrelationen (z.B. Zubehör innerhalb eines Hauptartikels) hierarchisch eingerückt an. +* **Fixes:** + * Die störende JavaScript `confirm()`-Meldung beim Löschen von Artikeln in der Übersicht (`articles.php`) wurde durch ein einheitliches, modernes Bootstrap-Modal ersetzt. diff --git a/src/articles.php b/src/articles.php index 3d8c6ab..dbf91d3 100644 --- a/src/articles.php +++ b/src/articles.php @@ -121,12 +121,28 @@ $stmt_man_load->execute(); $manufacturers_for_filter = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC); $stmt_man_load->close(); -$stmt_loc_load = $conn->prepare("SELECT id, name FROM storage_locations WHERE user_id IN ($placeholders) ORDER BY name ASC"); +$stmt_loc_load = $conn->prepare(" + SELECT l.id, l.name, l.parent_id, p.name as parent_name + FROM storage_locations l + LEFT JOIN storage_locations p ON l.parent_id = p.id + WHERE l.user_id IN ($placeholders) + ORDER BY IFNULL(p.name, l.name) ASC, l.parent_id IS NOT NULL, l.name ASC +"); $stmt_loc_load->bind_param($types, ...$household_member_ids); $stmt_loc_load->execute(); $storage_locations_for_bulk = $stmt_loc_load->get_result()->fetch_all(MYSQLI_ASSOC); $stmt_loc_load->close(); +// Build hierarchical options HTML +$location_options_html = ''; +foreach($storage_locations_for_bulk as $loc) { + if ($loc['parent_id']) { + $location_options_html .= ''; + } else { + $location_options_html .= ''; + } +} + if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['bulk_move_articles'])) { $new_location_id = intval($_POST['bulk_new_location']); $article_ids_raw = $_POST['bulk_article_ids'] ?? ''; @@ -687,6 +703,32 @@ document.addEventListener('DOMContentLoaded', function () { filterText.addEventListener('input', renderTable); filterCategory.addEventListener('change', renderTable); filterManufacturer.addEventListener('change', renderTable); + if(filterLocation) filterLocation.addEventListener('change', renderTable); + + window.confirmDelete = function(id, name) { + document.getElementById('delete_article_id_input').value = id; + document.getElementById('deleteArticleName').innerHTML = name; + new bootstrap.Modal(document.getElementById('deleteArticleModal')).show(); + }; + + 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(); +}); + + +le); + filterManufacturer.addEventListener('change', renderTable); document.getElementById('btn-expand-all').addEventListener('click', function() { collapsedCategories.clear(); diff --git a/src/public_location.php b/src/public_location.php index 7c88550..5750604 100644 --- a/src/public_location.php +++ b/src/public_location.php @@ -52,10 +52,56 @@ $stmt_articles = $conn->prepare($sql_articles); $stmt_articles->bind_param("ii", $location_id, $location_id); $stmt_articles->execute(); $result_articles = $stmt_articles->get_result(); -$articles = $result_articles->fetch_all(MYSQLI_ASSOC); +$articles_raw = $result_articles->fetch_all(MYSQLI_ASSOC); $stmt_articles->close(); $conn->close(); +// Hierarchische Gruppierung +$articlesById = []; +$articlesHierarchical = []; +foreach ($articles_raw as $article) { + $article['children'] = []; + $articlesById[$article['id']] = $article; +} +foreach ($articlesById as $id => &$article) { + if (!empty($article['parent_article_id']) && isset($articlesById[$article['parent_article_id']])) { + $articlesById[$article['parent_article_id']]['children'][] = &$article; + } else { + $articlesHierarchical[] = &$article; + } +} +unset($article); + +function renderArticleCard($article, $level = 0) { + $img = !empty($article['image_url']) ? htmlspecialchars($article['image_url']) : 'assets/images/keinbild.png'; + $meta = array_filter([$article['manufacturer_name'], $article['product_designation']]); + $meta_text = !empty($meta) ? htmlspecialchars(implode(' - ', $meta)) : 'Keine Details'; + $catColor = !empty($article['category_color']) ? htmlspecialchars($article['category_color']) : '#e2e8f0'; + $catName = !empty($article['category_name']) ? htmlspecialchars($article['category_name']) : 'Ohne Kategorie'; + + $marginLeft = $level * 20; // Einrückung in px + $html = ' +