Refactor packing list editor to Virtual Table workflow
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 15s
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 15s
- Created grid view for available articles (Lager) with +/- buttons. - Added middle pane for staging items (Auf dem Tisch). - Updated drag and drop sync to handle three phases. - Added direct links to Phase 1 and Phase 2 from the packing list overview.
This commit is contained in:
@@ -150,8 +150,7 @@ try {
|
||||
case 'delete_item':
|
||||
$item_id = intval($data['item_id']);
|
||||
|
||||
// Recursive Delete Logic using CTE if MySQL 8+ or manual recursion
|
||||
// Manual recursion to be safe on older DBs:
|
||||
// Recursive Delete Logic
|
||||
$ids_to_delete = [$item_id];
|
||||
$i = 0;
|
||||
while($i < count($ids_to_delete)) {
|
||||
@@ -167,27 +166,80 @@ try {
|
||||
$i++;
|
||||
}
|
||||
|
||||
// Delete all gathered IDs
|
||||
$in_query = implode(',', array_fill(0, count($ids_to_delete), '?'));
|
||||
$types = str_repeat('i', count($ids_to_delete));
|
||||
$stmt_delete = $conn->prepare("DELETE FROM packing_list_items WHERE id IN ($in_query) AND packing_list_id = ?");
|
||||
|
||||
// Combine ID params and list ID
|
||||
$delete_params = array_merge($ids_to_delete, [$packing_list_id]);
|
||||
$delete_types = $types . 'i';
|
||||
$stmt_delete->bind_param($delete_types, ...$delete_params);
|
||||
$stmt_delete->execute();
|
||||
$stmt_delete->close();
|
||||
break;
|
||||
|
||||
case 'adjust_table_quantity':
|
||||
$article_id = intval($data['article_id']);
|
||||
$delta = intval($data['delta']); // 1 or -1
|
||||
$include_children = !empty($data['include_children']);
|
||||
|
||||
function adjust_single($conn, $packing_list_id, $art_id, $delta) {
|
||||
$stmt_find = $conn->prepare("SELECT id, quantity FROM packing_list_items WHERE packing_list_id = ? AND article_id = ? AND carrier_user_id IS NULL AND backpack_id IS NULL AND backpack_compartment_id IS NULL AND parent_packing_list_item_id IS NULL LIMIT 1");
|
||||
$stmt_find->bind_param("ii", $packing_list_id, $art_id);
|
||||
$stmt_find->execute();
|
||||
$res = $stmt_find->get_result();
|
||||
if ($row = $res->fetch_assoc()) {
|
||||
$new_quantity = $row['quantity'] + $delta;
|
||||
if ($new_quantity > 0) {
|
||||
$stmt_update = $conn->prepare("UPDATE packing_list_items SET quantity = ? WHERE id = ?");
|
||||
$stmt_update->bind_param("ii", $new_quantity, $row['id']);
|
||||
$stmt_update->execute();
|
||||
$stmt_update->close();
|
||||
} else {
|
||||
$stmt_del = $conn->prepare("DELETE FROM packing_list_items WHERE id = ?");
|
||||
$stmt_del->bind_param("i", $row['id']);
|
||||
$stmt_del->execute();
|
||||
$stmt_del->close();
|
||||
}
|
||||
} else {
|
||||
if ($delta > 0) {
|
||||
$idx_res = $conn->query("SELECT MAX(order_index) as max_idx FROM packing_list_items WHERE packing_list_id = $packing_list_id");
|
||||
$next_idx = ($idx_res->fetch_assoc()['max_idx'] ?? 0) + 1;
|
||||
$stmt_insert = $conn->prepare("INSERT INTO packing_list_items (packing_list_id, article_id, quantity, order_index) VALUES (?, ?, ?, ?)");
|
||||
$stmt_insert->bind_param("iiii", $packing_list_id, $art_id, $delta, $next_idx);
|
||||
$stmt_insert->execute();
|
||||
$stmt_insert->close();
|
||||
}
|
||||
}
|
||||
$stmt_find->close();
|
||||
}
|
||||
|
||||
adjust_single($conn, $packing_list_id, $article_id, $delta);
|
||||
|
||||
if ($include_children && $delta > 0) {
|
||||
$stmt_children = $conn->prepare("SELECT id FROM articles WHERE parent_article_id = ?");
|
||||
$stmt_children->bind_param("i", $article_id);
|
||||
$stmt_children->execute();
|
||||
$res_child = $stmt_children->get_result();
|
||||
while($child = $res_child->fetch_assoc()) {
|
||||
adjust_single($conn, $packing_list_id, $child['id'], $delta);
|
||||
}
|
||||
$stmt_children->close();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update_quantity':
|
||||
$item_id = intval($data['item_id']);
|
||||
$quantity = intval($data['quantity']);
|
||||
if ($quantity > 0) {
|
||||
$stmt = $conn->prepare("UPDATE packing_list_items SET quantity = ? WHERE id = ?");
|
||||
$stmt->bind_param("ii", $quantity, $item_id); $stmt->execute(); $stmt->close();
|
||||
$conn->commit(); echo json_encode(['success' => true]); exit;
|
||||
$conn->commit(); echo json_encode(get_all_items($conn, $packing_list_id)); exit;
|
||||
} else { throw new Exception('Ungültige Mengenangabe.'); }
|
||||
break;
|
||||
|
||||
case 'get_items':
|
||||
// Just returns the current state (handled at the end of script)
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception('Unbekannte Aktion angefordert.', 400);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// manage_packing_list_items.php - Interaktive Verwaltung der Packlisten-Artikel
|
||||
// FINALE, REKURSIVE NESTED-SORTABLE VERSION
|
||||
// Refactored to "Virtual Table" workflow
|
||||
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
@@ -37,7 +37,13 @@ $is_in_same_household = ($is_household_list && $packing_list['household_id'] ==
|
||||
if ($is_owner || $is_in_same_household) { $can_edit = true; }
|
||||
if (!$can_edit) { die("Zugriff verweigert."); }
|
||||
|
||||
$page_title = "Packliste bearbeiten: " . htmlspecialchars($packing_list['name']);
|
||||
$phase = isset($_GET['phase']) ? intval($_GET['phase']) : 0;
|
||||
$col_class_lager = ($phase == 1) ? 'col-lg-6' : (($phase == 2) ? 'd-none' : 'col-lg-4');
|
||||
$col_class_table = ($phase == 1 || $phase == 2) ? 'col-lg-6' : 'col-lg-4';
|
||||
$col_class_rucksack = ($phase == 2) ? 'col-lg-6' : (($phase == 1) ? 'd-none' : 'col-lg-4');
|
||||
|
||||
$phase_title_suffix = ($phase == 1) ? " - Auswahl (Lager)" : (($phase == 2) ? " - Packen (Rucksack)" : "");
|
||||
$page_title = "Packliste bearbeiten: " . htmlspecialchars($packing_list['name']) . $phase_title_suffix;
|
||||
|
||||
$household_member_ids = [$current_user_id];
|
||||
if ($current_user_household_id) {
|
||||
@@ -92,14 +98,12 @@ $packed_items_raw = $stmt_items->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
$stmt_items->close();
|
||||
|
||||
$carriers_data = [];
|
||||
// KORREKTUR: Nur Träger anzeigen, die dieser Liste zugewiesen sind
|
||||
$stmt_carriers = $conn->prepare("SELECT u.id, u.username FROM users u JOIN packing_list_carriers plc ON u.id = plc.user_id WHERE plc.packing_list_id = ? ORDER BY u.username");
|
||||
$stmt_carriers->bind_param("i", $packing_list_id);
|
||||
$stmt_carriers->execute();
|
||||
$carriers_result = $stmt_carriers->get_result();
|
||||
while ($row = $carriers_result->fetch_assoc()) { $carriers_data[] = $row; }
|
||||
$stmt_carriers->close();
|
||||
$carriers_data[] = ['id' => null, 'username' => 'Sonstiges'];
|
||||
|
||||
$categories = array_unique(array_filter(array_column($all_articles_raw, 'category_name')));
|
||||
sort($categories);
|
||||
@@ -109,37 +113,131 @@ sort($manufacturers);
|
||||
$conn->close();
|
||||
?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||
<!-- Tom Select CSS/JS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
|
||||
|
||||
<div class="card">
|
||||
<style>
|
||||
.nested-sortable {
|
||||
min-height: 10px;
|
||||
padding-left: 20px;
|
||||
border-left: 2px solid rgba(0,0,0,0.05);
|
||||
margin-top: 5px;
|
||||
}
|
||||
.table-nested-sortable {
|
||||
min-height: 100px;
|
||||
padding: 10px;
|
||||
background: rgba(0,0,0,0.02);
|
||||
border: 1px dashed rgba(0,0,0,0.1);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.packed-item-container { margin-bottom: 5px; }
|
||||
.packed-item-content {
|
||||
display: flex; align-items: center; padding: 8px;
|
||||
background-color: #fff; border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 6px; transition: box-shadow 0.2s;
|
||||
}
|
||||
.packed-item-content:hover { box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
|
||||
.nested-sortable.sortable-ghost {
|
||||
background-color: rgba(59, 74, 35, 0.1); border: 1px dashed var(--color-primary);
|
||||
}
|
||||
.nested-sortable:empty {
|
||||
min-height: 30px; background-color: rgba(0,0,0,0.02);
|
||||
border: 1px dashed rgba(0,0,0,0.1); border-radius: 4px; margin-top: 5px;
|
||||
}
|
||||
.backpack-root-item { background-color: #e8f5e9; border: 1px solid #2e7d32; }
|
||||
.compartment-item { background-color: #f1f8e9; border-left: 3px solid #7cb342; }
|
||||
|
||||
/* Lager Grid */
|
||||
.lager-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
gap: 12px; padding: 5px;
|
||||
}
|
||||
.lager-card {
|
||||
border: 1px solid #eee; border-radius: 8px; padding: 12px;
|
||||
text-align: center; background: #fff; display: flex; flex-direction: column;
|
||||
transition: transform 0.1s, box-shadow 0.1s;
|
||||
}
|
||||
.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;
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
.lager-img-wrapper img {
|
||||
max-height: 100%; max-width: 100%; object-fit: contain; border-radius: 4px; cursor: pointer;
|
||||
}
|
||||
.lager-title {
|
||||
font-size: 0.85em; font-weight: 600; margin-bottom: 4px;
|
||||
line-height: 1.2; flex-grow: 1; word-wrap: break-word;
|
||||
}
|
||||
.lager-controls {
|
||||
display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 8px;
|
||||
}
|
||||
.table-status-text {
|
||||
font-size: 0.75em; color: #2e7d32; font-weight: 600;
|
||||
min-height: 1.2em; margin-top: 6px; visibility: hidden;
|
||||
}
|
||||
.table-status-text.visible { visibility: visible; }
|
||||
|
||||
.editor-pane { display: flex; flex-direction: column; height: 100%; }
|
||||
.pane-content { flex-grow: 1; overflow-y: auto; padding: 10px; }
|
||||
</style>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="h4 mb-0"><i class="fas fa-edit me-2"></i>Packliste bearbeiten: <?php echo htmlspecialchars($packing_list['name']); ?></h2>
|
||||
<a href="packing_list_detail.php?id=<?php echo $packing_list_id; ?>" class="btn btn-secondary"><i class="fas fa-eye me-2"></i>Zur Ansicht</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="editor-container">
|
||||
<div class="editor-pane">
|
||||
<div class="pane-header">
|
||||
<h5 class="mb-0">Verfügbare Artikel</h5>
|
||||
<div class="row g-2 mt-2">
|
||||
<div class="col-12"><input type="text" id="filter-text" class="form-control form-control-sm" placeholder="Artikel, Hersteller, Modell suchen..."></div>
|
||||
<div class="col-md-6"><select id="filter-category" class="form-select form-select-sm"><option value="">-- Alle Kategorien --</option><?php foreach($categories as $c) echo '<option>'.htmlspecialchars($c).'</option>'; ?></select></div>
|
||||
<div class="col-md-6"><select id="filter-manufacturer" class="form-select form-select-sm"><option value="">-- Alle Hersteller --</option><?php foreach($manufacturers as $m) echo '<option>'.htmlspecialchars($m).'</option>'; ?></select></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane-content" id="available-items-list"></div>
|
||||
</div>
|
||||
<div class="editor-pane">
|
||||
<div class="pane-header"><h5 class="mb-0">Gepackte Artikel</h5></div>
|
||||
<div class="pane-content" id="carriers-container"></div>
|
||||
</div>
|
||||
<h2 class="h4 mb-0"><i class="fas fa-edit me-2"></i><?php echo $page_title; ?></h2>
|
||||
<div>
|
||||
<?php if($phase == 1): ?>
|
||||
<a href="manage_packing_list_items.php?id=<?php echo $packing_list_id; ?>&phase=2" class="btn btn-success"><i class="fas fa-arrow-right me-2"></i>Weiter zu Phase 2 (Packen)</a>
|
||||
<?php elseif($phase == 2): ?>
|
||||
<a href="manage_packing_list_items.php?id=<?php echo $packing_list_id; ?>&phase=1" class="btn btn-info text-white"><i class="fas fa-arrow-left me-2"></i>Zurück zu Phase 1 (Auswahl)</a>
|
||||
<a href="packing_list_detail.php?id=<?php echo $packing_list_id; ?>" class="btn btn-secondary ms-2"><i class="fas fa-check me-2"></i>Fertig</a>
|
||||
<?php else: ?>
|
||||
<a href="packing_list_detail.php?id=<?php echo $packing_list_id; ?>" class="btn btn-secondary"><i class="fas fa-eye me-2"></i>Zur Ansicht</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3" style="height: 75vh; min-height: 600px;">
|
||||
<!-- LAGER -->
|
||||
<div class="<?php echo $col_class_lager; ?> h-100">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-light">
|
||||
<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>
|
||||
</div>
|
||||
<div class="card-body pane-content" id="lager-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TISCH -->
|
||||
<div class="<?php echo $col_class_table; ?> h-100">
|
||||
<div class="card h-100 border-info">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-table me-2"></i>Auf dem Tisch</h5>
|
||||
</div>
|
||||
<div class="card-body pane-content table-nested-sortable" id="table-container" data-carrier-id="null">
|
||||
<!-- Items dropped here get carrier_id=null -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RUCKSACK -->
|
||||
<div class="<?php echo $col_class_rucksack; ?> h-100">
|
||||
<div class="card h-100 border-success">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-backpack me-2"></i>Rucksäcke</h5>
|
||||
</div>
|
||||
<div class="card-body pane-content" id="carriers-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modals and Feedback remain unchanged -->
|
||||
<div class="modal fade" id="includeChildrenModal" tabindex="-1" aria-labelledby="includeChildrenModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
@@ -148,12 +246,12 @@ $conn->close();
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Dieser Artikel hat zugehörige Komponenten. Sollen diese ebenfalls zur Packliste hinzugefügt werden?
|
||||
<ul id="children-list" class="list-group mt-2"></ul>
|
||||
Dieser Artikel hat zugehörige Komponenten. Sollen diese ebenfalls auf den Tisch gelegt werden?
|
||||
<ul id="children-list" class="list-group mt-2 mb-2"></ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="add-without-children">Nein, nur diesen Artikel</button>
|
||||
<button type="button" class="btn btn-primary" id="add-with-children">Ja, alle hinzufügen</button>
|
||||
<button type="button" class="btn btn-primary" id="add-with-children">Ja, alle auf den Tisch</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,88 +260,22 @@ $conn->close();
|
||||
<div id="save-feedback" class="save-feedback">Änderungen gespeichert!</div>
|
||||
<div id="image-preview-tooltip" class="image-preview-tooltip"></div>
|
||||
|
||||
<style>
|
||||
/* Nested Sortable Specific Styles */
|
||||
.nested-sortable {
|
||||
min-height: 10px;
|
||||
padding-left: 20px; /* Indent child items */
|
||||
border-left: 2px solid rgba(0,0,0,0.05);
|
||||
margin-top: 5px;
|
||||
}
|
||||
.packed-item-container {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.packed-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
border-radius: 6px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.packed-item-content:hover {
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||||
}
|
||||
/* Highlight drop targets */
|
||||
.nested-sortable.sortable-ghost {
|
||||
background-color: rgba(59, 74, 35, 0.1);
|
||||
border: 1px dashed var(--color-primary);
|
||||
}
|
||||
/* Empty nested containers should have a hint of height to be droppable */
|
||||
.nested-sortable:empty {
|
||||
min-height: 30px;
|
||||
background-color: rgba(0,0,0,0.02);
|
||||
border: 1px dashed rgba(0,0,0,0.1);
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.nested-sortable:empty::after {
|
||||
content: 'Hier ablegen';
|
||||
display: block;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
font-size: 0.8em;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
/* Backpack & Compartment Styles */
|
||||
.backpack-root-item {
|
||||
background-color: #e8f5e9;
|
||||
border: 1px solid #2e7d32;
|
||||
}
|
||||
.compartment-item {
|
||||
background-color: #f1f8e9;
|
||||
border-left: 3px solid #7cb342;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let packedItems = <?php echo json_encode($packed_items_raw); ?>;
|
||||
const allArticles = <?php echo json_encode($all_articles_raw); ?>;
|
||||
const carriers = <?php echo json_encode($carriers_data); ?>;
|
||||
const packingListId = <?php echo $packing_list_id; ?>;
|
||||
let sortableInstances = [];
|
||||
let sortableInstances = {};
|
||||
let childrenModal = null;
|
||||
let pendingArticleIdForTable = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize Tom Select for filters with allowEmptyOption
|
||||
const tsOptions = {
|
||||
create: false,
|
||||
sortField: { field: "text", direction: "asc" },
|
||||
allowEmptyOption: true,
|
||||
onChange: function(value) {
|
||||
// Propagate change
|
||||
this.input.dispatchEvent(new Event('change'));
|
||||
}
|
||||
};
|
||||
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);
|
||||
|
||||
// Tooltip Logic
|
||||
const tooltip = document.getElementById('image-preview-tooltip');
|
||||
if (tooltip) {
|
||||
// Delegation for dynamically added items
|
||||
document.body.addEventListener('mouseover', e => {
|
||||
if (e.target.classList.contains('article-image-trigger')) {
|
||||
const url = e.target.getAttribute('data-preview-url');
|
||||
@@ -255,77 +287,155 @@ $conn->close();
|
||||
});
|
||||
document.body.addEventListener('mousemove', e => {
|
||||
if (tooltip.style.display === 'block') {
|
||||
tooltip.style.left = e.pageX + 15 + 'px';
|
||||
tooltip.style.top = e.pageY + 15 + 'px';
|
||||
tooltip.style.left = e.pageX + 15 + 'px'; tooltip.style.top = e.pageY + 15 + 'px';
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('mouseout', e => {
|
||||
if (e.target.classList.contains('article-image-trigger')) {
|
||||
tooltip.style.display = 'none';
|
||||
}
|
||||
if (e.target.classList.contains('article-image-trigger')) tooltip.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
['input', 'change'].forEach(evt => {
|
||||
document.getElementById('filter-text').addEventListener(evt, renderAvailableItems);
|
||||
document.getElementById('filter-category').addEventListener(evt, renderAvailableItems);
|
||||
document.getElementById('filter-manufacturer').addEventListener(evt, renderAvailableItems);
|
||||
document.getElementById('filter-text').addEventListener(evt, renderLager);
|
||||
document.getElementById('filter-category').addEventListener(evt, renderLager);
|
||||
document.getElementById('filter-manufacturer').addEventListener(evt, renderLager);
|
||||
});
|
||||
|
||||
document.getElementById('table-container').addEventListener('click', handlePackedItemActions);
|
||||
document.getElementById('carriers-container').addEventListener('click', handlePackedItemActions);
|
||||
document.getElementById('table-container').addEventListener('change', handleQuantityChange);
|
||||
document.getElementById('carriers-container').addEventListener('change', handleQuantityChange);
|
||||
|
||||
fullRender();
|
||||
});
|
||||
|
||||
function fullRender() {
|
||||
renderAvailableItems();
|
||||
renderLager();
|
||||
renderTable();
|
||||
renderCarriersAndPackedItems();
|
||||
}
|
||||
|
||||
function renderAvailableItems() {
|
||||
function renderLager() {
|
||||
const filterText = document.getElementById('filter-text').value.toLowerCase();
|
||||
const filterCategory = document.getElementById('filter-category').value;
|
||||
const filterManufacturer = document.getElementById('filter-manufacturer').value;
|
||||
const availableListEl = document.getElementById('available-items-list');
|
||||
|
||||
const packedQuantities = {};
|
||||
const tableQuantities = {};
|
||||
const backpackQuantities = {};
|
||||
|
||||
packedItems.forEach(item => {
|
||||
const aid = String(item.article_id);
|
||||
packedQuantities[aid] = (packedQuantities[aid] || 0) + parseInt(item.quantity || 1, 10);
|
||||
const qty = parseInt(item.quantity || 1, 10);
|
||||
const isTable = (item.carrier_user_id == null && !item.backpack_id && !item.backpack_compartment_id && !item.parent_packing_list_item_id);
|
||||
if (isTable) {
|
||||
tableQuantities[aid] = (tableQuantities[aid] || 0) + qty;
|
||||
} else {
|
||||
backpackQuantities[aid] = (backpackQuantities[aid] || 0) + qty;
|
||||
}
|
||||
});
|
||||
|
||||
let html = '';
|
||||
let html = '<div class="lager-grid">';
|
||||
allArticles.forEach(article => {
|
||||
if (article.parent_article_id) return;
|
||||
|
||||
const aid = String(article.id);
|
||||
const packedQty = packedQuantities[aid] || 0;
|
||||
const ownedQty = parseInt(article.quantity_owned || 1, 10);
|
||||
const isConsumable = article.consumable == 1;
|
||||
|
||||
if (isConsumable || packedQty < ownedQty) {
|
||||
const matchesText = article.name.toLowerCase().includes(filterText) || (article.manufacturer_name && article.manufacturer_name.toLowerCase().includes(filterText)) || (article.product_designation && article.product_designation.toLowerCase().includes(filterText));
|
||||
const matchesCategory = !filterCategory || article.category_name === filterCategory;
|
||||
const matchesManufacturer = !filterManufacturer || article.manufacturer_name === filterManufacturer;
|
||||
if (matchesText && matchesCategory && matchesManufacturer) {
|
||||
const details = [article.manufacturer_name, article.product_designation].filter(Boolean).join(' - ');
|
||||
const imgUrl = article.image_url ? article.image_url : 'assets/images/keinbild.png';
|
||||
html += `
|
||||
<div class="available-item-card d-flex align-items-center" data-article-id="${article.id}">
|
||||
<img src="${imgUrl}" class="article-image-trigger me-2 rounded" style="width: 30px; height: 30px; object-fit: cover; cursor: pointer;" data-preview-url="${imgUrl}">
|
||||
<div class="flex-grow-1">
|
||||
${article.name}
|
||||
<small class="text-muted d-block" style="line-height: 1;">(${details || '---'} | ${article.weight_grams}g)</small>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
const matchesText = article.name.toLowerCase().includes(filterText) || (article.manufacturer_name && article.manufacturer_name.toLowerCase().includes(filterText)) || (article.product_designation && article.product_designation.toLowerCase().includes(filterText));
|
||||
const matchesCategory = !filterCategory || article.category_name === filterCategory;
|
||||
const matchesManufacturer = !filterManufacturer || article.manufacturer_name === filterManufacturer;
|
||||
|
||||
if (matchesText && matchesCategory && matchesManufacturer) {
|
||||
const aid = String(article.id);
|
||||
const qtyTable = tableQuantities[aid] || 0;
|
||||
const qtyBackpack = backpackQuantities[aid] || 0;
|
||||
const qtyTotal = qtyTable + qtyBackpack;
|
||||
const ownedQty = parseInt(article.quantity_owned || 1, 10);
|
||||
const isConsumable = article.consumable == 1;
|
||||
|
||||
const disablePlus = !isConsumable && qtyTotal >= ownedQty;
|
||||
const disableMinus = qtyTable <= 0;
|
||||
|
||||
const imgUrl = article.image_url ? article.image_url : 'assets/images/keinbild.png';
|
||||
const plusBtnClass = qtyTable > 0 ? 'btn-success' : 'btn-outline-primary';
|
||||
|
||||
html += `
|
||||
<div class="lager-card">
|
||||
<div class="lager-img-wrapper">
|
||||
<img src="${imgUrl}" class="article-image-trigger" data-preview-url="${imgUrl}">
|
||||
</div>
|
||||
<div class="lager-title">${article.name}</div>
|
||||
<small class="text-muted d-block mb-1">${article.weight_grams}g</small>
|
||||
<div class="lager-controls">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="adjustTable(${aid}, -1)" ${disableMinus ? 'disabled' : ''}><i class="fas fa-minus"></i></button>
|
||||
<button class="btn btn-sm ${plusBtnClass}" onclick="triggerAddTable(${aid})" ${disablePlus ? 'disabled' : ''}><i class="fas fa-plus"></i></button>
|
||||
</div>
|
||||
<div class="table-status-text ${qtyTable > 0 ? 'visible' : ''}">
|
||||
${qtyTable} auf dem Tisch
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
availableListEl.innerHTML = html;
|
||||
html += '</div>';
|
||||
document.getElementById('lager-container').innerHTML = html;
|
||||
}
|
||||
|
||||
function triggerAddTable(articleId) {
|
||||
const aidStr = String(articleId);
|
||||
const childArticles = allArticles.filter(a => String(a.parent_article_id) === aidStr);
|
||||
if (childArticles.length > 0) {
|
||||
pendingArticleIdForTable = articleId;
|
||||
if (!childrenModal) childrenModal = new bootstrap.Modal(document.getElementById('includeChildrenModal'));
|
||||
document.getElementById('children-list').innerHTML = childArticles.map(c => `<li class="list-group-item py-1">${c.name} <small class="text-muted">(${c.weight_grams}g)</small></li>`).join('');
|
||||
|
||||
document.getElementById('add-with-children').onclick = () => {
|
||||
childrenModal.hide();
|
||||
adjustTable(pendingArticleIdForTable, 1, true);
|
||||
};
|
||||
document.getElementById('add-without-children').onclick = () => {
|
||||
childrenModal.hide();
|
||||
adjustTable(pendingArticleIdForTable, 1, false);
|
||||
};
|
||||
childrenModal.show();
|
||||
} else {
|
||||
adjustTable(articleId, 1, false);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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]; });
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const container = document.getElementById('table-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
new Sortable(availableListEl, {
|
||||
group: { name: 'nested', pull: 'clone', put: false },
|
||||
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);
|
||||
|
||||
if(sortableInstances['table']) sortableInstances['table'].destroy();
|
||||
sortableInstances['table'] = new Sortable(container, {
|
||||
group: 'nested',
|
||||
animation: 150,
|
||||
sort: false
|
||||
handle: '.handle',
|
||||
fallbackOnBody: true,
|
||||
swapThreshold: 0.65,
|
||||
ghostClass: 'sortable-ghost',
|
||||
onEnd: function() { syncListState(); }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -333,22 +443,21 @@ $conn->close();
|
||||
const container = document.getElementById('carriers-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
sortableInstances.forEach(s => s.destroy());
|
||||
sortableInstances = [];
|
||||
Object.keys(sortableInstances).forEach(k => { if(k !== 'table') sortableInstances[k].destroy(); });
|
||||
|
||||
carriers.forEach(carrier => {
|
||||
const carrierId = carrier.id === null ? 'null' : carrier.id;
|
||||
const carrierId = carrier.id; // Sonstiges removed, so carrier.id is never null
|
||||
const carrierDiv = document.createElement('div');
|
||||
carrierDiv.className = 'carrier-box';
|
||||
carrierDiv.innerHTML = `<div class="carrier-header"><h6>${carrier.username}</h6></div>`;
|
||||
carrierDiv.className = 'carrier-box mb-3';
|
||||
carrierDiv.innerHTML = `<div class="p-2 bg-light border rounded mb-2 fw-bold text-dark"><i class="fas fa-user me-2"></i>${carrier.username}</div>`;
|
||||
|
||||
const carrierRootList = document.createElement('div');
|
||||
carrierRootList.className = 'carrier-list nested-sortable';
|
||||
carrierRootList.className = 'carrier-list nested-sortable pb-3';
|
||||
carrierRootList.dataset.carrierId = carrierId;
|
||||
carrierDiv.appendChild(carrierRootList);
|
||||
container.appendChild(carrierDiv);
|
||||
|
||||
const itemsForCarrier = packedItems.filter(item => (item.carrier_user_id === null ? 'null' : String(item.carrier_user_id)) == String(carrierId));
|
||||
const itemsForCarrier = packedItems.filter(item => String(item.carrier_user_id) === String(carrierId));
|
||||
const itemsById = itemsForCarrier.reduce((acc, item) => ({...acc, [item.id]: {...item, children: []}}), {});
|
||||
const rootItems = [];
|
||||
|
||||
@@ -360,20 +469,41 @@ $conn->close();
|
||||
}
|
||||
});
|
||||
|
||||
renderRecursive(rootItems, carrierRootList);
|
||||
initNestedSortable(carrierRootList);
|
||||
renderRecursive(rootItems, carrierRootList, itemsById);
|
||||
|
||||
sortableInstances['carrier_'+carrierId] = new Sortable(carrierRootList, {
|
||||
group: 'nested',
|
||||
animation: 150,
|
||||
handle: '.handle',
|
||||
fallbackOnBody: true,
|
||||
swapThreshold: 0.65,
|
||||
ghostClass: 'sortable-ghost',
|
||||
onEnd: function() { syncListState(); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderRecursive(items, container) {
|
||||
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);
|
||||
}
|
||||
|
||||
const itemEl = createPackedItemDOM(item);
|
||||
container.appendChild(itemEl);
|
||||
const nestedContainer = itemEl.querySelector('.nested-sortable');
|
||||
if (item.children && item.children.length > 0) {
|
||||
renderRecursive(item.children, nestedContainer);
|
||||
if (children && children.length > 0) {
|
||||
renderRecursive(children, nestedContainer, contextMap);
|
||||
}
|
||||
initNestedSortable(nestedContainer);
|
||||
|
||||
sortableInstances['nested_'+item.id] = new Sortable(nestedContainer, {
|
||||
group: 'nested', animation: 150, handle: '.handle',
|
||||
fallbackOnBody: true, swapThreshold: 0.65, ghostClass: 'sortable-ghost',
|
||||
onEnd: function() { syncListState(); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -386,36 +516,31 @@ $conn->close();
|
||||
if(item.backpack_compartment_id) div.dataset.backpackCompartmentId = item.backpack_compartment_id;
|
||||
|
||||
let contentClass = 'packed-item-content';
|
||||
let iconClass = 'fas fa-grip-vertical handle';
|
||||
let iconClass = 'fas fa-grip-vertical handle text-secondary';
|
||||
let nameDisplay = item.name;
|
||||
let metaDisplay = item.manufacturer_name ? `<small class="text-muted">(${item.manufacturer_name})</small>` : '';
|
||||
let controls = `
|
||||
<input type="number" class="form-control form-control-sm quantity-input text-center mx-2" value="${item.quantity}" min="1">
|
||||
<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-danger remove-item-btn"><i class="fas fa-trash"></i></button>
|
||||
`;
|
||||
|
||||
// Special styling for Containers
|
||||
if (item.backpack_id) {
|
||||
contentClass += ' backpack-root-item';
|
||||
iconClass = 'fas fa-hiking handle me-2';
|
||||
iconClass = 'fas fa-hiking handle me-2 text-success';
|
||||
nameDisplay = `<strong>${item.name}</strong>`;
|
||||
metaDisplay = '';
|
||||
// Disable controls for structural items
|
||||
controls = '';
|
||||
metaDisplay = ''; controls = '';
|
||||
} else if (item.backpack_compartment_id) {
|
||||
contentClass += ' compartment-item';
|
||||
iconClass = 'fas fa-folder-open handle me-2 text-success';
|
||||
nameDisplay = `<span class="fw-bold text-dark">${item.name}</span>`;
|
||||
metaDisplay = '';
|
||||
// Disable controls for structural items
|
||||
controls = '';
|
||||
metaDisplay = ''; controls = '';
|
||||
}
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="${contentClass}">
|
||||
<i class="${iconClass}"></i>
|
||||
<i class="${iconClass}" style="cursor:grab;"></i>
|
||||
<span class="item-name ms-2">${nameDisplay} ${metaDisplay}</span>
|
||||
<div class="item-controls">
|
||||
<div class="item-controls ms-auto d-flex align-items-center">
|
||||
${controls}
|
||||
</div>
|
||||
</div>
|
||||
@@ -424,131 +549,43 @@ $conn->close();
|
||||
return div;
|
||||
}
|
||||
|
||||
function initNestedSortable(element) {
|
||||
const s = new Sortable(element, {
|
||||
group: 'nested',
|
||||
animation: 150,
|
||||
handle: '.handle',
|
||||
fallbackOnBody: true,
|
||||
swapThreshold: 0.65,
|
||||
ghostClass: 'sortable-ghost',
|
||||
onAdd: function (evt) {
|
||||
if (evt.from.id === 'available-items-list') {
|
||||
const articleId = evt.item.dataset.articleId;
|
||||
const originalArticleData = allArticles.find(a => String(a.id) === articleId);
|
||||
|
||||
const newItemData = {
|
||||
id: 'new-' + Date.now(),
|
||||
article_id: articleId,
|
||||
name: originalArticleData.name,
|
||||
manufacturer_name: originalArticleData.manufacturer_name,
|
||||
quantity: 1,
|
||||
consumable: originalArticleData.consumable,
|
||||
children: []
|
||||
};
|
||||
|
||||
const newDOM = createPackedItemDOM(newItemData);
|
||||
evt.item.replaceWith(newDOM);
|
||||
initNestedSortable(newDOM.querySelector('.nested-sortable'));
|
||||
|
||||
const childArticles = allArticles.filter(a => String(a.parent_article_id) === articleId);
|
||||
if (childArticles.length > 0) {
|
||||
handleChildArticlesPrompt(childArticles, newDOM);
|
||||
} else {
|
||||
syncListState();
|
||||
}
|
||||
} else {
|
||||
syncListState();
|
||||
}
|
||||
},
|
||||
onEnd: function (evt) {
|
||||
if (evt.from.id !== 'available-items-list') {
|
||||
syncListState();
|
||||
}
|
||||
}
|
||||
});
|
||||
sortableInstances.push(s);
|
||||
}
|
||||
|
||||
function handleChildArticlesPrompt(childArticles, parentElement) {
|
||||
if (!childrenModal) childrenModal = new bootstrap.Modal(document.getElementById('includeChildrenModal'));
|
||||
document.getElementById('children-list').innerHTML = childArticles.map(c => `<li class="list-group-item">${c.name}</li>`).join('');
|
||||
|
||||
document.getElementById('add-with-children').onclick = () => {
|
||||
childrenModal.hide();
|
||||
const nestedContainer = parentElement.querySelector('.nested-sortable');
|
||||
childArticles.forEach(child => {
|
||||
const childItemData = {
|
||||
id: 'new-child-' + Date.now() + Math.random(),
|
||||
article_id: child.id,
|
||||
name: child.name,
|
||||
manufacturer_name: child.manufacturer_name,
|
||||
quantity: 1,
|
||||
consumable: child.consumable,
|
||||
children: []
|
||||
};
|
||||
const childDOM = createPackedItemDOM(childItemData);
|
||||
nestedContainer.appendChild(childDOM);
|
||||
initNestedSortable(childDOM.querySelector('.nested-sortable'));
|
||||
});
|
||||
syncListState();
|
||||
};
|
||||
|
||||
document.getElementById('add-without-children').onclick = () => {
|
||||
childrenModal.hide();
|
||||
syncListState();
|
||||
};
|
||||
childrenModal.show();
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
function traverse(container, 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, pliId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
traverse(rootList, null);
|
||||
traverse(rootList, carrierId, null);
|
||||
});
|
||||
|
||||
sendApiRequest(payload).then(newItems => {
|
||||
packedItems = newItems;
|
||||
// Save scroll positions
|
||||
const scrollPos = {
|
||||
avail: document.getElementById('available-items-list').scrollTop,
|
||||
carrier: document.getElementById('carriers-container').scrollTop
|
||||
};
|
||||
if(Array.isArray(newItems)) packedItems = newItems;
|
||||
const panes = document.querySelectorAll('.pane-content');
|
||||
const scrollMap = Array.from(panes).map(p => p.scrollTop);
|
||||
|
||||
fullRender();
|
||||
|
||||
// Restore scroll positions
|
||||
Array.from(panes).forEach((p, i) => {
|
||||
if (scrollMap[i] !== undefined) p.scrollTop = scrollMap[i];
|
||||
});
|
||||
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.");
|
||||
@@ -563,9 +600,12 @@ $conn->close();
|
||||
const itemId = itemEl.dataset.itemId;
|
||||
|
||||
if (button.classList.contains('remove-item-btn')) {
|
||||
if (confirm('Diesen Artikel wirklich aus der Packliste entfernen?')) {
|
||||
if (confirm('Diesen Artikel wirklich aus der Liste entfernen?')) {
|
||||
itemEl.remove();
|
||||
sendApiRequest({ action: 'delete_item', item_id: itemId });
|
||||
sendApiRequest({ action: 'delete_item', item_id: itemId }).then(newItems => {
|
||||
if(Array.isArray(newItems)) packedItems = newItems;
|
||||
fullRender();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,7 +618,10 @@ $conn->close();
|
||||
const newQuantity = input.value;
|
||||
clearTimeout(input.dataset.timeout);
|
||||
input.dataset.timeout = setTimeout(() => {
|
||||
sendApiRequest({ action: 'update_quantity', item_id: itemId, quantity: newQuantity });
|
||||
sendApiRequest({ action: 'update_quantity', item_id: itemId, quantity: newQuantity }).then(newItems => {
|
||||
if(Array.isArray(newItems)) packedItems = newItems;
|
||||
fullRender();
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
@@ -597,7 +640,7 @@ $conn->close();
|
||||
return Promise.reject(errorData);
|
||||
}
|
||||
const json = await response.json();
|
||||
if (data.action !== 'update_quantity') {
|
||||
if (data.action !== 'update_quantity' && data.action !== 'get_items') {
|
||||
const fb = document.getElementById('save-feedback');
|
||||
fb.style.opacity = '1';
|
||||
setTimeout(() => { fb.style.opacity = '0'; }, 1500);
|
||||
|
||||
@@ -156,7 +156,9 @@ $conn->close();
|
||||
<a href="packing_list_detail.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-success" title="Details ansehen"><i class="fas fa-eye"></i></a>
|
||||
<?php if ($is_owner || $can_edit_household_list): ?>
|
||||
<a href="edit_packing_list_details.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-primary" title="Details bearbeiten"><i class="fas fa-pencil-alt"></i></a>
|
||||
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Artikel verwalten"><i class="fas fa-boxes"></i></a>
|
||||
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>&phase=1" class="btn btn-sm btn-outline-info" title="Phase 1: Artikel aufsammeln (Lager -> Tisch)"><i class="fas fa-hand-pointer"></i> 1. Aufsammeln</a>
|
||||
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>&phase=2" class="btn btn-sm btn-outline-success" title="Phase 2: Rucksack packen (Tisch -> Rucksack)"><i class="fas fa-backpack"></i> 2. Packen</a>
|
||||
<a href="manage_packing_list_items.php?id=<?php echo $list['id']; ?>" class="btn btn-sm btn-outline-secondary" title="Alle 3 Spalten anzeigen"><i class="fas fa-columns"></i></a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($is_owner): ?>
|
||||
|
||||
Reference in New Issue
Block a user