Fix workflow: restore split increments, fix print view, add move modal, adjust Phase 1 card heights
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 14s

- Reverted adjust_single to use UPDATE when an item is already on the table, fixing the first-click bug and preventing duplicate rows in Phase 1.

- Added a modal 'Move' button to items in Phase 2 allowing them to be moved between backpacks and compartments directly.

- Further reduced the height of Lager cards to remove empty space below buttons.

- Fixed the print view by updating the SQL query and adding manufacturer + product designation to the rows.
This commit is contained in:
Gemini Agent
2026-05-12 11:45:27 +00:00
parent c4abd9db0e
commit bf71ad7990
2 changed files with 92 additions and 15 deletions

View File

@@ -159,17 +159,17 @@ $conn->close();
border: 1px solid #eee; border-radius: 8px; padding: 6px;
text-align: center; background: #fff; display: flex; flex-direction: column;
transition: transform 0.1s, box-shadow 0.1s;
height: 200px;
height: 165px;
}
.lager-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.05); }
.lager-img-wrapper {
width: 100%; height: 90px; margin-bottom: 6px; display: flex;
width: 100%; height: 75px; margin-bottom: 4px; display: flex;
align-items: center; justify-content: center; overflow: hidden;
flex-shrink: 0;
}
.lager-img-wrapper img.lager-card-img {
width: 100% !important; height: 100% !important;
max-width: 100% !important; max-height: 90px !important;
max-width: 100% !important; max-height: 75px !important;
object-fit: contain !important; border-radius: 4px;
}
.lager-title {
@@ -178,12 +178,12 @@ $conn->close();
flex-shrink: 0;
}
.lager-meta {
font-size: 0.7em; color: #6c757d; margin-bottom: 4px;
font-size: 0.7em; color: #6c757d; margin-bottom: 2px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
line-height: 1.1; flex-shrink: 0;
}
.lager-controls {
display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: auto;
display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: auto; margin-bottom: 2px;
}
.table-status-text {
font-size: 0.75em; color: #2e7d32; font-weight: 600;
@@ -345,6 +345,27 @@ $conn->close();
</div>
</div>
<div class="modal fade" id="moveItemModal" tabindex="-1" aria-labelledby="moveItemModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="moveItemModalLabel">Artikel verschieben</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Wohin möchtest du diesen Artikel verschieben?</p>
<select id="move-destination-select" class="form-select">
<!-- Populated via JS -->
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" id="btn-move-save">Verschieben</button>
</div>
</div>
</div>
</div>
<div id="save-feedback" class="save-feedback">Änderungen gespeichert!</div>
<div id="image-preview-tooltip" class="image-preview-tooltip"></div>
@@ -566,14 +587,6 @@ $conn->close();
function adjustTable(articleId, delta, includeChildren = false) {
sendApiRequest({ action: 'adjust_table_quantity', article_id: articleId, delta: delta, include_children: includeChildren })
.then(() => {
return fetch('api_packing_list_handler.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'get_items', packing_list_id: packingListId }) // Fallback to get items
});
})
.then(res => res.json())
.then(newItems => {
if(Array.isArray(newItems)) packedItems = newItems;
// Restore scroll positions
@@ -733,7 +746,8 @@ $conn->close();
let metaDisplay = metaDisplayArr.length > 0 ? `<small class="text-muted">(${metaDisplayArr.join(' - ')})</small>` : '';
let controls = `
<input type="number" class="form-control form-control-sm quantity-input text-center mx-2" value="${item.quantity}" min="1" style="width:60px;">
<button class="btn btn-sm btn-outline-secondary move-item-btn" title="Verschieben"><i class="fas fa-exchange-alt"></i></button>
<input type="number" class="form-control form-control-sm quantity-input text-center mx-1" value="${item.quantity}" min="1" style="width:50px;">
<button class="btn btn-sm btn-outline-danger remove-item-btn"><i class="fas fa-trash"></i></button>
`;
@@ -811,6 +825,8 @@ $conn->close();
}, 300);
}
let itemToMoveId = null;
let itemToMoveEl = null;
let itemToRemoveId = null;
let itemToRemoveEl = null;
@@ -821,6 +837,59 @@ $conn->close();
if (!itemEl) return;
const itemId = itemEl.dataset.itemId;
if (button.classList.contains('move-item-btn')) {
itemToMoveId = itemId;
itemToMoveEl = itemEl;
const selectEl = document.getElementById('move-destination-select');
selectEl.innerHTML = '<option value="table">Zurück auf den Tisch</option>';
// Populate select with all containers
const containers = packedItems.filter(item => item.backpack_id || item.backpack_compartment_id);
// First add backpacks
containers.filter(c => c.backpack_id).forEach(bp => {
const opt = document.createElement('option');
opt.value = bp.id;
opt.textContent = `👜 Rucksack: ${bp.name}`;
// Get the carrier Name
const carrier = carriers.find(c => String(c.id) === String(bp.carrier_user_id));
if (carrier) opt.textContent += ` (${carrier.username})`;
selectEl.appendChild(opt);
// Add its compartments
containers.filter(c => c.parent_packing_list_item_id === bp.id).forEach(comp => {
const optComp = document.createElement('option');
optComp.value = comp.id;
optComp.textContent = ` ↳ 📁 Fach: ${comp.name}`;
selectEl.appendChild(optComp);
});
});
if (!window.moveItemModalInstance) {
window.moveItemModalInstance = new bootstrap.Modal(document.getElementById('moveItemModal'));
document.getElementById('btn-move-save').onclick = () => {
const destId = document.getElementById('move-destination-select').value;
window.moveItemModalInstance.hide();
if (destId === 'table') {
document.getElementById('table-container').appendChild(itemToMoveEl);
} else {
// Find the container element
const destEl = document.querySelector(`.packed-item-container[data-item-id="${destId}"] > .nested-sortable`);
if (destEl) {
destEl.appendChild(itemToMoveEl);
} else {
alert("Fehler: Ziel-Fach nicht in der aktuellen Ansicht gefunden.");
return;
}
}
syncListState();
};
}
window.moveItemModalInstance.show();
return;
}
if (button.classList.contains('remove-item-btn')) {
const isTable = (itemEl.closest('#table-container') !== null);
if (isTable) {

View File

@@ -48,9 +48,11 @@ if ($packing_list) {
pli.carrier_user_id, pli.backpack_id, pli.backpack_compartment_id,
COALESCE(a.name, pli.name, bp.name, bpc.name, 'Unbekannt') AS name,
a.weight_grams, c.name AS category_name, a.consumable, a.image_url,
a.product_designation, m.name AS manufacturer_name,
u.username AS carrier_name
FROM packing_list_items pli
LEFT JOIN articles a ON pli.article_id = a.id
LEFT JOIN manufacturers m ON a.manufacturer_id = m.id
LEFT JOIN categories c ON a.category_id = c.id
LEFT JOIN backpacks bp ON pli.backpack_id = bp.id
LEFT JOIN backpack_compartments bpc ON pli.backpack_compartment_id = bpc.id
@@ -129,7 +131,13 @@ function render_rows_recursive($items, $level = 0) {
// Name
echo '<td style="padding-left: ' . $indent . 'px;">';
if ($level > 0 && !$is_container) echo '<span style="color:#ccc; margin-right:5px;">↳</span>';
echo '<span style="' . $name_style . '">' . htmlspecialchars($item['name']) . '</span>';
$meta = [];
if (!empty($item['manufacturer_name'])) $meta[] = $item['manufacturer_name'];
if (!empty($item['product_designation'])) $meta[] = $item['product_designation'];
$meta_str = !empty($meta) && !$is_container ? ' <small style="color:#666;">(' . htmlspecialchars(implode(' - ', $meta)) . ')</small>' : '';
echo '<span style="' . $name_style . '">' . htmlspecialchars($item['name']) . '</span>' . $meta_str;
echo '</td>';
// Qty