Feat: UI Verbesserungen für Lagerorte und Artikel
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 39s
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 39s
- Hierarchische Anzeige der Lagerorte im Verschieben-Dropdown - Neuer Lagerort-Filter für Artikel - Lösch-Bestätigung durch Bootstrap-Modal ersetzt - Größenanpassung und Rahmen für QR-Code Druck - Hierarchische Artikel-Anzeige in der öffentlichen Box-Ansicht - Changelog aktualisiert
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 .= '<option value="'.$loc['id'].'"> ↳ '.htmlspecialchars($loc['name']).'</option>';
|
||||
} else {
|
||||
$location_options_html .= '<option value="'.$loc['id'].'">'.htmlspecialchars($loc['name']).'</option>';
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once 'footer.php'; ?>le);
|
||||
filterManufacturer.addEventListener('change', renderTable);
|
||||
|
||||
document.getElementById('btn-expand-all').addEventListener('click', function() {
|
||||
collapsedCategories.clear();
|
||||
|
||||
@@ -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 = '
|
||||
<div class="article-card" style="margin-left: '.$marginLeft.'px; border-left: '.($level > 0 ? '3px solid #dee2e6' : 'none').'; border-top-left-radius: '.($level > 0 ? '0' : '8px').'; border-bottom-left-radius: '.($level > 0 ? '0' : '8px').';">
|
||||
<img src="'.$img.'" class="article-img" alt="Bild">
|
||||
<div class="article-info">
|
||||
<div class="article-title">'.htmlspecialchars($article['name']).'</div>
|
||||
<div class="article-meta mb-1">'.$meta_text.'</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge rounded-pill border" style="background-color: '.$catColor.'; color: #fff; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; font-size: 0.7rem;">'.$catName.'</span>
|
||||
<span class="text-muted" style="font-size: 0.75rem;"><i class="fas fa-weight-hanging me-1"></i>'.$article['weight_grams'].'g</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qty-badge">'.$article['quantity_owned'].'x</div>
|
||||
</div>';
|
||||
|
||||
if (!empty($article['children'])) {
|
||||
foreach ($article['children'] as $child) {
|
||||
$html .= renderArticleCard($child, $level + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
@@ -364,6 +364,10 @@ function printDiv(divId) {
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body bg-light">
|
||||
<div class="mb-4 bg-white p-3 border rounded d-print-none">
|
||||
<label for="qrSizeInput" class="form-label fw-bold">QR-Code Höhe/Breite (in mm):</label>
|
||||
<input type="number" id="qrSizeInput" class="form-control" value="40" min="10" max="100" onchange="renderAllBarcodes()">
|
||||
</div>
|
||||
<div class="row" id="allBarcodesContainer"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -394,3 +398,4 @@ function printDiv(divId) {
|
||||
</div>
|
||||
|
||||
<?php require_once 'footer.php'; ?>
|
||||
php require_once 'footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user