Fix UI feedback on virtual table refactor
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 12s

- Increased image size in Lager grid.

- Allowed dragging by clicking anywhere on the item row.

- Removed empty space/dropzone gaps on the table.

- Added debounce to syncListState to prevent concurrent save errors during rapid drag & drop.
This commit is contained in:
Gemini Agent
2026-05-11 16:11:21 +00:00
parent 862b567284
commit 8fea9e11a6

View File

@@ -160,7 +160,7 @@ $conn->close();
}
.lager-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.05); }
.lager-img-wrapper {
height: 80px; margin-bottom: 8px; display: flex;
height: 130px; margin-bottom: 8px; display: flex;
align-items: center; justify-content: center;
}
.lager-img-wrapper img {
@@ -181,6 +181,21 @@ $conn->close();
.editor-pane { display: flex; flex-direction: column; height: 100%; }
.pane-content { flex-grow: 1; overflow-y: auto; padding: 10px; }
/* Remove empty dropzone on the table specifically */
#table-container > .packed-item-container > .nested-sortable:empty {
min-height: 0;
border: none;
background: transparent;
margin: 0;
}
#table-container > .packed-item-container > .nested-sortable:empty::after {
display: none;
}
/* Grab cursor for items */
.packed-item-content { cursor: grab; }
.packed-item-content:active { cursor: grabbing; }
</style>
<div class="card mb-3">
@@ -431,7 +446,7 @@ $conn->close();
sortableInstances['table'] = new Sortable(container, {
group: 'nested',
animation: 150,
handle: '.handle',
handle: '.packed-item-content',
fallbackOnBody: true,
swapThreshold: 0.65,
ghostClass: 'sortable-ghost',
@@ -474,7 +489,7 @@ $conn->close();
sortableInstances['carrier_'+carrierId] = new Sortable(carrierRootList, {
group: 'nested',
animation: 150,
handle: '.handle',
handle: '.packed-item-content',
fallbackOnBody: true,
swapThreshold: 0.65,
ghostClass: 'sortable-ghost',
@@ -485,8 +500,6 @@ $conn->close();
function renderRecursive(items, container, contextMap) {
items.forEach(item => {
// For flat items array from table, build children tree if needed, but table shouldn't have nested items naturally unless dragged.
// But if they are dragged out, they might keep children.
let children = item.children || [];
if (!item.children && Array.isArray(contextMap)) {
children = contextMap.filter(i => i.parent_packing_list_item_id === item.id);
@@ -500,7 +513,7 @@ $conn->close();
}
sortableInstances['nested_'+item.id] = new Sortable(nestedContainer, {
group: 'nested', animation: 150, handle: '.handle',
group: 'nested', animation: 150, handle: '.packed-item-content',
fallbackOnBody: true, swapThreshold: 0.65, ghostClass: 'sortable-ghost',
onEnd: function() { syncListState(); }
});
@@ -538,7 +551,7 @@ $conn->close();
div.innerHTML = `
<div class="${contentClass}">
<i class="${iconClass}" style="cursor:grab;"></i>
<i class="${iconClass}"></i>
<span class="item-name ms-2">${nameDisplay} ${metaDisplay}</span>
<div class="item-controls ms-auto d-flex align-items-center">
${controls}
@@ -549,47 +562,51 @@ $conn->close();
return div;
}
let syncTimeout = null;
function syncListState() {
const payload = { action: 'sync_list', list: [] };
function traverse(container, carrierId, parentId) {
Array.from(container.children).forEach(child => {
if (!child.classList.contains('packed-item-container')) return;
const pliId = child.dataset.itemId;
const articleId = child.dataset.articleId;
payload.list.push({
pli_id: pliId,
article_id: articleId,
carrier_id: carrierId,
parent_pli_id: parentId,
backpack_id: child.dataset.backpackId || null,
backpack_compartment_id: child.dataset.backpackCompartmentId || null
if (syncTimeout) clearTimeout(syncTimeout);
syncTimeout = setTimeout(() => {
const payload = { action: 'sync_list', list: [] };
function traverse(container, carrierId, parentId) {
Array.from(container.children).forEach(child => {
if (!child.classList.contains('packed-item-container')) return;
const pliId = child.dataset.itemId;
const articleId = child.dataset.articleId;
payload.list.push({
pli_id: pliId,
article_id: articleId,
carrier_id: carrierId,
parent_pli_id: parentId,
backpack_id: child.dataset.backpackId || null,
backpack_compartment_id: child.dataset.backpackCompartmentId || null
});
const nestedContainer = child.querySelector('.nested-sortable');
if (nestedContainer) traverse(nestedContainer, carrierId, pliId);
});
const nestedContainer = child.querySelector('.nested-sortable');
if (nestedContainer) traverse(nestedContainer, carrierId, pliId);
}
const tableList = document.getElementById('table-container');
if (tableList) traverse(tableList, 'null', null);
document.querySelectorAll('.carrier-list').forEach(rootList => {
const carrierId = rootList.dataset.carrierId;
traverse(rootList, carrierId, null);
});
}
const tableList = document.getElementById('table-container');
if (tableList) traverse(tableList, 'null', null);
document.querySelectorAll('.carrier-list').forEach(rootList => {
const carrierId = rootList.dataset.carrierId;
traverse(rootList, carrierId, null);
});
sendApiRequest(payload).then(newItems => {
if(Array.isArray(newItems)) packedItems = newItems;
const panes = document.querySelectorAll('.pane-content');
const scrollMap = Array.from(panes).map(p => p.scrollTop);
fullRender();
Array.from(panes).forEach((p, i) => { if (scrollMap[i] !== undefined) p.scrollTop = scrollMap[i]; });
}).catch(err => {
console.error("Sync failed:", err);
alert("Fehler beim Speichern! Bitte Seite neu laden.");
});
sendApiRequest(payload).then(newItems => {
if(Array.isArray(newItems)) packedItems = newItems;
const panes = document.querySelectorAll('.pane-content');
const scrollMap = Array.from(panes).map(p => p.scrollTop);
fullRender();
Array.from(panes).forEach((p, i) => { if (scrollMap[i] !== undefined) p.scrollTop = scrollMap[i]; });
}).catch(err => {
console.error("Sync failed:", err);
alert("Fehler beim Speichern! Bitte Seite neu laden.");
});
}, 300);
}
function handlePackedItemActions(e) {