Fix Sortable destroy error and add Group By filter
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 18s

- Safely destroy Sortable instances before clearing innerHTML.

- Added Group By dropdown (Kategorie, Hersteller, Lagerort) to Phase 1.

- Increased image size in Lager grid from 130px to 160px.
This commit is contained in:
Gemini Agent
2026-05-12 06:02:13 +00:00
parent 8cd5536dab
commit 53b07fafa9

View File

@@ -57,10 +57,11 @@ if ($current_user_household_id) {
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
$sql_all_articles = "SELECT a.id, a.name, a.weight_grams, a.quantity_owned, a.product_designation, a.consumable, a.parent_article_id, a.image_url, c.name as category_name, m.name as manufacturer_name
$sql_all_articles = "SELECT a.id, a.name, a.weight_grams, a.quantity_owned, a.product_designation, a.consumable, a.parent_article_id, a.image_url, c.name as category_name, m.name as manufacturer_name, sl.name as storage_location_name
FROM articles a
LEFT JOIN categories c ON a.category_id = c.id
LEFT JOIN manufacturers m ON a.manufacturer_id = m.id
LEFT JOIN storage_locations sl ON a.storage_location_id = sl.id
WHERE a.user_id IN ($placeholders)";
$all_params = $household_member_ids;
$all_types = $types;
@@ -150,7 +151,7 @@ $conn->close();
/* Lager Grid */
.lager-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px; padding: 5px;
}
.lager-card {
@@ -160,7 +161,7 @@ $conn->close();
}
.lager-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.05); }
.lager-img-wrapper {
height: 130px; margin-bottom: 8px; display: flex;
height: 160px; margin-bottom: 8px; display: flex;
align-items: center; justify-content: center;
}
.lager-img-wrapper img {
@@ -222,8 +223,9 @@ $conn->close();
<h5 class="mb-2"><i class="fas fa-boxes me-2"></i>Lagerbestand</h5>
<div class="row g-2">
<div class="col-12"><input type="text" id="filter-text" class="form-control form-control-sm" placeholder="Artikel suchen..."></div>
<div class="col-6"><select id="filter-category" class="form-select form-select-sm"><option value="">-- Kategorien --</option><?php foreach($categories as $c) echo '<option>'.htmlspecialchars($c).'</option>'; ?></select></div>
<div class="col-6"><select id="filter-manufacturer" class="form-select form-select-sm"><option value="">-- Hersteller --</option><?php foreach($manufacturers as $m) echo '<option>'.htmlspecialchars($m).'</option>'; ?></select></div>
<div class="col-4"><select id="filter-category" class="form-select form-select-sm"><option value="">-- Kategorien --</option><?php foreach($categories as $c) echo '<option>'.htmlspecialchars($c).'</option>'; ?></select></div>
<div class="col-4"><select id="filter-manufacturer" class="form-select form-select-sm"><option value="">-- Hersteller --</option><?php foreach($manufacturers as $m) echo '<option>'.htmlspecialchars($m).'</option>'; ?></select></div>
<div class="col-4"><select id="group-by" class="form-select form-select-sm"><option value="">Keine Gruppe</option><option value="category_name">Kategorie</option><option value="manufacturer_name">Hersteller</option><option value="storage_location_name">Lagerort</option></select></div>
</div>
</div>
<div class="card-body pane-content" id="lager-container"></div>
@@ -288,6 +290,7 @@ $conn->close();
const tsOptions = { create: false, sortField: { field: "text", direction: "asc" }, allowEmptyOption: true, onChange: function() { this.input.dispatchEvent(new Event('change')); } };
new TomSelect('#filter-category', tsOptions);
new TomSelect('#filter-manufacturer', tsOptions);
new TomSelect('#group-by', tsOptions);
const tooltip = document.getElementById('image-preview-tooltip');
if (tooltip) {
@@ -314,6 +317,7 @@ $conn->close();
document.getElementById('filter-text').addEventListener(evt, renderLager);
document.getElementById('filter-category').addEventListener(evt, renderLager);
document.getElementById('filter-manufacturer').addEventListener(evt, renderLager);
document.getElementById('group-by').addEventListener(evt, renderLager);
});
document.getElementById('table-container').addEventListener('click', handlePackedItemActions);
@@ -334,6 +338,7 @@ $conn->close();
const filterText = document.getElementById('filter-text').value.toLowerCase();
const filterCategory = document.getElementById('filter-category').value;
const filterManufacturer = document.getElementById('filter-manufacturer').value;
const groupBy = document.getElementById('group-by').value;
const tableQuantities = {};
const backpackQuantities = {};
@@ -349,7 +354,8 @@ $conn->close();
}
});
let html = '<div class="lager-grid">';
let groups = {};
allArticles.forEach(article => {
if (article.parent_article_id) return;
@@ -371,7 +377,7 @@ $conn->close();
const imgUrl = article.image_url ? article.image_url : 'assets/images/keinbild.png';
const plusBtnClass = qtyTable > 0 ? 'btn-success' : 'btn-outline-primary';
html += `
const cardHtml = `
<div class="lager-card">
<div class="lager-img-wrapper">
<img src="${imgUrl}" class="article-image-trigger" data-preview-url="${imgUrl}">
@@ -386,9 +392,18 @@ $conn->close();
${qtyTable} auf dem Tisch
</div>
</div>`;
let groupKey = groupBy ? (article[groupBy] || 'Ohne Zuordnung') : 'Alle Artikel';
if (!groups[groupKey]) groups[groupKey] = '';
groups[groupKey] += cardHtml;
}
});
html += '</div>';
let html = '';
Object.keys(groups).sort().forEach(key => {
if (groupBy) html += `<h6 class="w-100 mt-3 mb-2 ps-2 border-bottom pb-1 text-secondary">${key}</h6>`;
html += `<div class="lager-grid">${groups[key]}</div>`;
});
document.getElementById('lager-container').innerHTML = html;
}
@@ -436,13 +451,22 @@ $conn->close();
function renderTable() {
const container = document.getElementById('table-container');
Object.keys(sortableInstances).forEach(k => {
if (k === 'table' || k.startsWith('table_nested_')) {
if(sortableInstances[k]) {
try { sortableInstances[k].destroy(); } catch(e){}
}
delete sortableInstances[k];
}
});
container.innerHTML = '';
const tableItems = packedItems.filter(item => item.carrier_user_id == null && !item.backpack_id && !item.backpack_compartment_id && !item.parent_packing_list_item_id);
renderRecursive(tableItems, container, packedItems);
renderRecursive(tableItems, container, packedItems, 'table_');
if(sortableInstances['table']) sortableInstances['table'].destroy();
sortableInstances['table'] = new Sortable(container, {
group: 'nested',
animation: 150,
@@ -456,9 +480,17 @@ $conn->close();
function renderCarriersAndPackedItems() {
const container = document.getElementById('carriers-container');
container.innerHTML = '';
Object.keys(sortableInstances).forEach(k => { if(k !== 'table') sortableInstances[k].destroy(); });
Object.keys(sortableInstances).forEach(k => {
if (k.startsWith('carrier_')) {
if(sortableInstances[k]) {
try { sortableInstances[k].destroy(); } catch(e){}
}
delete sortableInstances[k];
}
});
container.innerHTML = '';
carriers.forEach(carrier => {
const carrierId = carrier.id; // Sonstiges removed, so carrier.id is never null
@@ -484,7 +516,7 @@ $conn->close();
}
});
renderRecursive(rootItems, carrierRootList, itemsById);
renderRecursive(rootItems, carrierRootList, itemsById, 'carrier_');
sortableInstances['carrier_'+carrierId] = new Sortable(carrierRootList, {
group: 'nested',
@@ -498,7 +530,7 @@ $conn->close();
});
}
function renderRecursive(items, container, contextMap) {
function renderRecursive(items, container, contextMap, prefix) {
items.forEach(item => {
let children = item.children || [];
if (!item.children && Array.isArray(contextMap)) {
@@ -509,10 +541,10 @@ $conn->close();
container.appendChild(itemEl);
const nestedContainer = itemEl.querySelector('.nested-sortable');
if (children && children.length > 0) {
renderRecursive(children, nestedContainer, contextMap);
renderRecursive(children, nestedContainer, contextMap, prefix);
}
sortableInstances['nested_'+item.id] = new Sortable(nestedContainer, {
sortableInstances[prefix + 'nested_'+item.id] = new Sortable(nestedContainer, {
group: 'nested', animation: 150, handle: '.packed-item-content',
fallbackOnBody: true, swapThreshold: 0.65, ghostClass: 'sortable-ghost',
onEnd: function() { syncListState(); }