diff --git a/README.md b/README.md index 31147e2..058733e 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,9 @@ Das Projekt basiert auf bewährten Web-Standards: * Neuer Menüpunkt zur Verwaltung (Bearbeiten, Löschen, Hinzufügen) von Kategorien analog zu den Herstellern implementiert. * Kategorien können nun mit individuellen Farben (Hex-Code per Color-Picker) versehen werden. Diese Farben werden in der Detailansicht von Packlisten sowie in der Kachelansicht von Artikeln als farbige Badges dargestellt. * Das Tortendiagramm auf dem Dashboard (`index.php`) nutzt nun ebenfalls die individuellen Kategorie-Farben. - * Dashboard (`index.php`) um zusätzliche Haushalts-Statistiken erweitert (Gesamtzahl Artikel, Packlisten, Vorlagen, Rucksäcke). + * Dashboard (`index.php`) um zusätzliche Haushalts-Statistiken erweitert (Gesamtzahl Artikel, Packlisten, Vorlagen, Rucksäcke). Packlistenvorlagen werden aus dem Gewicht-Diagramm ausgeschlossen. * Kachelhöhe in der Artikel-Übersicht und im Packlisten-Editor (`manage_packing_list_items.php`) leicht angepasst. Hover-Bilder in Kacheln entfernt und leere Felder symmetrisch ausgerichtet. * Anzeigename wird nun auch auf dem Dashboard (`index.php`) und in der Kategorienverwaltung korrekt angezeigt. + * 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. diff --git a/src/articles.php b/src/articles.php index 819f943..c4eef7d 100644 --- a/src/articles.php +++ b/src/articles.php @@ -121,6 +121,49 @@ $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) OR household_id = ? ORDER BY name ASC"); +$stmt_loc_load->bind_param($all_types, ...$all_params); +$stmt_loc_load->execute(); +$storage_locations_for_bulk = $stmt_loc_load->get_result()->fetch_all(MYSQLI_ASSOC); +$stmt_loc_load->close(); + +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'] ?? ''; + if (!empty($article_ids_raw) && $new_location_id > 0) { + $article_ids = array_map('intval', explode(',', $article_ids_raw)); + $article_ids = array_filter($article_ids, fn($id) => $id > 0); + if (count($article_ids) > 0) { + $id_placeholders = implode(',', array_fill(0, count($article_ids), '?')); + $types = str_repeat('i', count($article_ids)); + $update_sql = "UPDATE articles SET storage_location_id = ? WHERE id IN ($id_placeholders) AND (user_id = ? OR household_id = ?)"; + $stmt_update = $conn->prepare($update_sql); + $params = array_merge([$new_location_id], $article_ids, [$current_user_id, $current_user_household_id]); + $param_types = 'i' . $types . 'ii'; + $stmt_update->bind_param($param_types, ...$params); + if ($stmt_update->execute()) { + // To display the message, we don't redirect but let it render, or we redirect with a session message. + // Since there is no session message system, we just let the page render with the message. + $message = ''; + + // Need to reload articles so the view is updated + $stmt = $conn->prepare($sql); + $stmt->bind_param($all_types, ...$all_params); + $stmt->execute(); + $result = $stmt->get_result(); + $articles = []; + while ($row = $result->fetch_assoc()) { + $articles[] = $row; + } + $stmt->close(); + } else { + $message = ''; + } + $stmt_update->close(); + } + } +} + $conn->close(); ?> @@ -151,12 +194,19 @@ $conn->close(); +
+ @@ -203,6 +253,33 @@ $conn->close(); + + - @@ -464,8 +543,10 @@ document.addEventListener('DOMContentLoaded', function () { const indentStyle = level > 0 ? `padding-left: ${1.5 * level}rem;` : ''; const treePrefix = level > 0 ? `` : ''; const nameCellContent = `
${treePrefix}${article.name}
`; + const checkboxHtml = (isOwner || isHouseholdArticle) ? `` : ''; html += ` + @@ -561,4 +642,52 @@ document.addEventListener('DOMContentLoaded', function () { }); +tAll.checked = false; + newSelectAll.addEventListener('change', function() { + const isChecked = this.checked; + document.querySelectorAll('.bulk-select-checkbox').forEach(cb => { + cb.checked = isChecked; + if (isChecked) bulkSelectedArticles.add(cb.value); + else bulkSelectedArticles.delete(cb.value); + }); + updateBulkUI(); + }); + } + updateBulkUI(); + } + + const bulkSelectedArticles = new Set(); + function updateBulkUI() { + const count = bulkSelectedArticles.size; + document.getElementById('bulk-selected-count').textContent = count; + const container = document.getElementById('bulk-action-container'); + if (count > 0 && currentView === 'list') { + container.style.display = ''; + const idsInput = document.getElementById('bulk_article_ids_input'); + if(idsInput) idsInput.value = Array.from(bulkSelectedArticles).join(','); + } else { + container.style.display = 'none'; + } + } + + 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(); +}); + + \ No newline at end of file diff --git a/src/db_connect.php b/src/db_connect.php index 3fd4bd3..35951b2 100644 --- a/src/db_connect.php +++ b/src/db_connect.php @@ -62,4 +62,10 @@ $check_category_color = $conn->query("SHOW COLUMNS FROM categories LIKE 'color'" if ($check_category_color && $check_category_color->num_rows == 0) { $conn->query("ALTER TABLE categories ADD COLUMN color VARCHAR(7) DEFAULT '#e2e8f0'"); } + +// Ensure public_token exists in storage_locations +$check_loc_token = $conn->query("SHOW COLUMNS FROM storage_locations LIKE 'public_token'"); +if ($check_loc_token && $check_loc_token->num_rows == 0) { + $conn->query("ALTER TABLE storage_locations ADD COLUMN public_token VARCHAR(64) UNIQUE DEFAULT NULL"); +} ?> diff --git a/src/index.php b/src/index.php index 177ee96..20e80ff 100644 --- a/src/index.php +++ b/src/index.php @@ -63,7 +63,7 @@ if ($stmt_articles_by_category) { } $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"; +$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.is_template = 0 AND (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]); diff --git a/src/manage_packing_list_items.php b/src/manage_packing_list_items.php index 69fb2da..4308f3c 100644 --- a/src/manage_packing_list_items.php +++ b/src/manage_packing_list_items.php @@ -38,6 +38,17 @@ if ($is_owner || $is_in_same_household) { $can_edit = true; } if (!$can_edit) { die("Zugriff verweigert."); } $phase = isset($_GET['phase']) ? intval($_GET['phase']) : 0; + +if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['empty_table'])) { + // Delete items that are "on the table" (not assigned to any backpack or carrier) + $stmt_del = $conn->prepare("DELETE FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id IS NULL AND backpack_id IS NULL AND backpack_compartment_id IS NULL"); + $stmt_del->bind_param("i", $packing_list_id); + $stmt_del->execute(); + $stmt_del->close(); + header("Location: manage_packing_list_items.php?id=" . $packing_list_id . "&phase=" . $phase); + exit; +} + $col_class_lager = ($phase == 1) ? 'col-phase1-lager' : (($phase == 2) ? 'd-none' : 'col-lg-4'); $col_class_table = ($phase == 1) ? 'col-phase1-table' : (($phase == 2) ? 'col-lg-6' : 'col-lg-4'); $col_class_rucksack = ($phase == 2) ? 'col-lg-6' : (($phase == 1) ? 'd-none' : 'col-lg-4'); diff --git a/src/public_location.php b/src/public_location.php new file mode 100644 index 0000000..7c88550 --- /dev/null +++ b/src/public_location.php @@ -0,0 +1,130 @@ +prepare("SELECT id, name, parent_id FROM storage_locations WHERE public_token = ?"); +$stmt_loc->bind_param("s", $token); +$stmt_loc->execute(); +$result_loc = $stmt_loc->get_result(); + +if ($result_loc->num_rows === 0) { + die("Lagerort nicht gefunden oder Link abgelaufen."); +} + +$location = $result_loc->fetch_assoc(); +$stmt_loc->close(); +$location_id = $location['id']; + +// Lade ggf. die Ebene 1, falls dies Ebene 2 ist +$parent_name = ''; +if ($location['parent_id']) { + $stmt_parent = $conn->prepare("SELECT name FROM storage_locations WHERE id = ?"); + $stmt_parent->bind_param("i", $location['parent_id']); + $stmt_parent->execute(); + $res_parent = $stmt_parent->get_result(); + if ($res_parent->num_rows > 0) { + $parent_name = $res_parent->fetch_assoc()['name']; + } + $stmt_parent->close(); +} + +$location_display_name = htmlspecialchars($location['name']); +if (!empty($parent_name)) { + $location_display_name = htmlspecialchars($parent_name) . ' ' . $location_display_name; +} + +// Lade Artikel in diesem Lagerort (und in Unterorten, falls Ebene 1) +$sql_articles = "SELECT a.name, a.weight_grams, a.quantity_owned, a.image_url, a.product_designation, m.name as manufacturer_name, c.name as category_name, c.color as category_color + FROM articles a + LEFT JOIN manufacturers m ON a.manufacturer_id = m.id + LEFT JOIN categories c ON a.category_id = c.id + LEFT JOIN storage_locations l ON a.storage_location_id = l.id + WHERE a.storage_location_id = ? OR l.parent_id = ? + ORDER BY c.name ASC, a.name ASC"; +$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); +$stmt_articles->close(); +$conn->close(); + +?> + + + + + + Inhalt: <?php echo strip_tags($location_display_name); ?> + + + + + + + + + +
+
+
Lagerort
+
+
+
+
+ +
+ +
+ +

Dieser Lagerort ist leer.

+
+ +

Artikel gefunden

+ + +
+ Bild + +
x
+
+ + +
+ +
+ Trekking Packliste App +
+ + + \ No newline at end of file diff --git a/src/storage_locations.php b/src/storage_locations.php index 6b66b4b..4269149 100644 --- a/src/storage_locations.php +++ b/src/storage_locations.php @@ -112,8 +112,22 @@ elseif (isset($_GET['action']) && $_GET['action'] == 'delete' && isset($_GET['id } +// AKTION: BARCODE GENERIEREN +elseif (isset($_GET['action']) && $_GET['action'] == 'generate_barcode' && isset($_GET['id'])) { + $location_id = intval($_GET['id']); + $token = bin2hex(random_bytes(16)); + $stmt_token = $conn->prepare("UPDATE storage_locations SET public_token = ? WHERE id = ? AND user_id IN ($placeholders)"); + $all_params = array_merge([$token, $location_id], $household_member_ids); + $all_types = 'si' . $types; + $stmt_token->bind_param($all_types, ...$all_params); + $stmt_token->execute(); + $stmt_token->close(); + header("Location: storage_locations.php"); + exit; +} + // 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"; +$sql = "SELECT sl.id, sl.name, sl.parent_id, sl.user_id, sl.public_token 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(); @@ -180,6 +194,11 @@ foreach ($all_locations as $location) {
+ + + + +
@@ -249,3 +268,10 @@ document.addEventListener('DOMContentLoaded', function() { +ationName; + }); + } +}); + + +
Bild Name
+ ${catName} ${count}
${checkboxHtml} ${article.name} ${productLink} ${nameCellContent}