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 = '