Feat: Enhanced backpack selection UI in add/edit packing list and fixed tree toggle

This commit is contained in:
Gemini Agent
2025-12-05 19:03:57 +00:00
parent c0271b7360
commit 77959374a9
4 changed files with 251 additions and 218 deletions

View File

@@ -59,20 +59,35 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
if ($stmt->execute()) {
$new_list_id = $conn->insert_id;
// Handle Backpacks
if (isset($_POST['backpacks'])) {
foreach ($_POST['backpacks'] as $uid => $bid) {
// Handle Backpacks and Participation
if (isset($_POST['participate']) && is_array($_POST['participate'])) {
foreach ($_POST['participate'] as $uid => $val) {
$uid = intval($uid);
$bid = intval($bid);
if ($bid > 0) {
// Only add if checked (value is usually '1')
if ($val) {
$bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0;
// Insert Carrier (even if no backpack is assigned, they are a carrier on the list)
// But DB schema: packing_list_carriers links user to list. Backpack is optional.
// If we want them on the list, we insert.
// Check if $bid is valid (>0)
$bid_to_insert = ($bid > 0) ? $bid : NULL;
$stmt_in = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)");
$stmt_in->bind_param("iii", $new_list_id, $uid, $bid);
$stmt_in->bind_param("iii", $new_list_id, $uid, $bid_to_insert);
$stmt_in->execute();
$stmt_in->close();
sync_backpack_items($conn, $new_list_id, $uid, $bid);
if ($bid > 0) {
sync_backpack_items($conn, $new_list_id, $uid, $bid);
}
}
}
} else {
// If no array (e.g. single user form without checkboxes?), usually we force current user?
// Or assume no one selected. Let's ensure Current User is added if not explicitly unchecked?
// Actually, the UI below will default to checked for everyone.
}
if ($household_id_for_user) {
@@ -104,7 +119,7 @@ require_once 'header.php';
<form action="add_packing_list.php" method="post">
<div class="row">
<div class="col-md-8">
<div class="col-md-7">
<div class="mb-3">
<label for="name" class="form-label"><i class="fas fa-file-signature me-2 text-muted"></i>Name der Packliste</label>
<input type="text" class="form-control" id="name" value="<?php echo htmlspecialchars($name); ?>" name="name" required>
@@ -122,71 +137,55 @@ require_once 'header.php';
</div>
<?php endif; ?>
</div>
<div class="col-md-4">
<h5 class="mb-3"><i class="fas fa-hiking me-2 text-muted"></i>Rucksack-Zuweisung</h5>
<div class="col-md-5">
<h5 class="mb-3"><i class="fas fa-users me-2 text-muted"></i>Teilnehmer & Rucksäcke</h5>
<div class="card bg-light border-0">
<div class="card-body">
<p class="small text-muted">Wähle gleich hier, wer welchen Rucksack trägt.</p>
<p class="small text-muted">Wähle aus, wer mitkommt und wer welchen Rucksack trägt.</p>
<?php foreach ($available_users as $user):
<?php
$user_backpacks_json = [];
$all_assigned_backpack_ids = []; // Empty for new list
foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $household_id_for_user);
// Pre-select: None (0)
$current_bp_id = 0;
// Store for JS
$user_backpacks_json[$user['id']] = [
'current_id' => $current_bp_id,
'backpacks' => $user_backpacks
];
?>
<div class="mb-3">
<label class="form-label fw-bold small"><?php echo htmlspecialchars($user['username']); ?></label>
<select class="form-select form-select-sm" name="backpacks[<?php echo $user['id']; ?>]">
<option value="0">-- Kein Rucksack --</option>
<?php foreach ($user_backpacks as $bp): ?>
<option value="<?php echo $bp['id']; ?>">
<?php echo htmlspecialchars($bp['name']); ?>
</option>
<?php endforeach; ?>
</select>
<div class="mb-3 pb-3 border-bottom">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="participate[<?php echo $user['id']; ?>]" value="1" checked id="part_<?php echo $user['id']; ?>">
<label class="form-check-label fw-bold" for="part_<?php echo $user['id']; ">
<?php echo htmlspecialchars($user['username']); ?>
</label>
</div>
<div class="ms-4">
<?php echo render_backpack_card_selector($user, $current_bp_id, $user_backpacks); ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary"><i class="fas fa-plus-circle me-2"></i>Packliste erstellen</button>
<a href="packing_lists.php" class="btn btn-secondary"><i class="fas fa-arrow-left me-2"></i>Abbrechen</a>
</div>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between mt-4">
<button type="submit" class="btn btn-primary"><i class="fas fa-plus-circle me-2"></i>Packliste erstellen & Artikel hinzufügen</button>
<a href="packing_lists.php" class="btn btn-secondary"><i class="fas fa-arrow-left me-2"></i>Abbrechen</a>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const selects = document.querySelectorAll('select[name^="backpacks"]');
function updateOptions() {
const selectedValues = Array.from(selects)
.map(s => s.value)
.filter(v => v !== "0");
selects.forEach(select => {
const currentVal = select.value;
Array.from(select.options).forEach(option => {
if (option.value === "0") return;
// If this option is selected in another dropdown, hide it
// Unless it's the currently selected value of THIS dropdown
if (selectedValues.includes(option.value) && option.value !== currentVal) {
option.style.display = 'none';
option.disabled = true; // For robust blocking
} else {
option.style.display = '';
option.disabled = false;
}
});
});
}
selects.forEach(s => s.addEventListener('change', updateOptions));
updateOptions(); // Init on load
});
</script>
</div>
<?php require_once 'footer.php'; ?>
<!-- Render Modal JS -->
<?php echo render_backpack_modal_script($user_backpacks_json, $all_assigned_backpack_ids); ?>
<?php require_once 'footer.php'; ?>

View File

@@ -70,4 +70,171 @@ function get_available_backpacks_for_user($conn, $target_user_id, $household_id)
}
return $bps;
}
/**
* Renders the HTML for the Backpack Selection Card (Small widget per user)
*/
function render_backpack_card_selector($user, $current_bp_id, $user_backpacks) {
// Find current details
$current_bp_details = null;
foreach ($user_backpacks as $bp) {
if ($bp['id'] == $current_bp_id) {
$current_bp_details = $bp;
break;
}
}
ob_start();
?>
<input type="hidden" name="backpacks[<?php echo $user['id']; ?>]" id="input_bp_<?php echo $user['id']; ?>" value="<?php echo $current_bp_id; ?>">
<div class="d-flex align-items-center">
<!-- Selected Backpack Display -->
<div id="display_bp_<?php echo $user['id']; ?>" class="flex-grow-1 p-2 border rounded bg-white d-flex align-items-center" style="min-height: 60px;">
<?php if ($current_bp_details): ?>
<?php if(!empty($current_bp_details['image_url'])): ?>
<img src="<?php echo htmlspecialchars($current_bp_details['image_url']); ?>" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" class="me-2">
<?php else: ?>
<i class="fas fa-hiking fa-2x text-muted me-2"></i>
<?php endif; ?>
<div>
<div class="fw-bold small"><?php echo htmlspecialchars($current_bp_details['name']); ?></div>
</div>
<?php else: ?>
<span class="text-muted small fst-italic">Kein Rucksack zugewiesen</span>
<?php endif; ?>
</div>
<button type="button" class="btn btn-sm btn-outline-primary ms-2" onclick="openBackpackModal(<?php echo $user['id']; ?>, '<?php echo htmlspecialchars($user['username']); ?>')">
<i class="fas fa-exchange-alt"></i>
</button>
</div>
<?php
return ob_get_clean();
}
/**
* Renders the Modal HTML and the JS needed to drive it.
* Needs $user_backpacks_json and $all_assigned_backpack_ids to be populated.
*/
function render_backpack_modal_script($user_backpacks_json, $all_assigned_backpack_ids) {
ob_start();
?>
<!-- Generic Modal -->
<div class="modal fade" id="genericBpModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="genericBpModalTitle">Rucksack wählen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body bg-light" id="genericBpModalBody">
<!-- Content loaded via JS -->
</div>
</div>
</div>
</div>
<script>
const userBackpacksData = <?php echo json_encode($user_backpacks_json); ?>;
const allAssignedIds = <?php echo json_encode($all_assigned_backpack_ids); ?>;
function openBackpackModal(userId, username) {
const data = userBackpacksData[userId];
if (!data) return; // Should not happen
const currentBpId = data.current_id;
const titleEl = document.getElementById('genericBpModalTitle');
const bodyEl = document.getElementById('genericBpModalBody');
titleEl.textContent = 'Rucksack für ' + username + ' wählen';
let html = '<div class="row g-3">';
// Option: None
html += `
<div class="col-md-4 col-6">
<div class="card h-100 text-center p-3 bp-select-card ${currentBpId == 0 ? 'border-primary bg-white' : ''}" onclick="selectBackpack(${userId}, 0)">
<div class="card-body">
<i class="fas fa-times-circle fa-2x text-muted mb-2"></i>
<div class="small fw-bold">Kein Rucksack</div>
</div>
</div>
</div>
`;
data.backpacks.forEach(bp => {
// Filter logic: Show if NOT assigned OR if assigned to ME
const isAssigned = allAssignedIds.some(id => String(id) === String(bp.id));
// Display logic: Show if free OR if it is my current one
if (isAssigned && String(bp.id) !== String(currentBpId)) return;
const imgHtml = bp.image_url ? `<img src="${bp.image_url}" class="card-img-top" style="height: 100px; object-fit: contain; padding: 10px;">` : `<div class="text-center py-4"><i class="fas fa-hiking fa-3x text-muted"></i></div>`;
const activeClass = (String(bp.id) === String(currentBpId)) ? 'border-primary bg-white' : '';
// Escape name for HTML attribute
const safeName = bp.name.replace(/"/g, '&quot;');
html += `
<div class="col-md-4 col-6">
<div class="card h-100 bp-select-card ${activeClass}" onclick='selectBackpack(${userId}, ${bp.id})'>
${imgHtml}
<div class="card-body p-2 text-center">
<div class="small fw-bold text-truncate">${bp.name}</div>
</div>
</div>
</div>
`;
});
html += '</div>';
bodyEl.innerHTML = html;
new bootstrap.Modal(document.getElementById('genericBpModal')).show();
}
function selectBackpack(userId, bpId) {
// Update Hidden Input
document.getElementById('input_bp_' + userId).value = bpId;
// Find BP Data for display
let bpName = 'Kein Rucksack zugewiesen';
let bpImage = null;
if (bpId > 0 && userBackpacksData[userId]) {
const data = userBackpacksData[userId];
const bp = data.backpacks.find(b => String(b.id) === String(bpId));
if (bp) {
bpName = bp.name;
bpImage = bp.image_url;
}
}
// Update Display
const displayDiv = document.getElementById('display_bp_' + userId);
let html = '';
if (bpId == 0) {
html = '<span class="text-muted small fst-italic">Kein Rucksack zugewiesen</span>';
} else {
if (bpImage) {
html += '<img src="' + bpImage + '" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" class="me-2">';
} else {
html += '<i class="fas fa-hiking fa-2x text-muted me-2"></i>';
}
html += '<div><div class="fw-bold small">' + bpName + '</div></div>';
}
displayDiv.innerHTML = html;
// Close Modal
const modalEl = document.getElementById('genericBpModal');
const modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
}
</script>
<style>
.bp-select-card { cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; border: 1px solid rgba(0,0,0,0.1); }
.bp-select-card:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); border-color: var(--color-primary); }
</style>
<?php
return ob_get_clean();
}
?>

View File

@@ -189,7 +189,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
// Calculate used backpacks
$all_assigned_backpack_ids = array_values($current_assignments);
// Prepare data for JS
// Prepare data for JS (still needed for the render helper)
$user_backpacks_json = [];
foreach ($available_users as $user):
@@ -202,42 +202,13 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
'backpacks' => $user_backpacks
];
// Find current backpack details for display
$current_bp_details = null;
foreach ($user_backpacks as $bp) {
if ($bp['id'] == $my_current_bp_id) {
$current_bp_details = $bp;
break;
}
}
// Use helper to render the widget
echo '<div class="mb-4 border-bottom pb-3">';
echo '<label class="form-label fw-bold mb-2">' . htmlspecialchars($user['username']) . '</label>';
echo render_backpack_card_selector($user, $my_current_bp_id, $user_backpacks);
echo '</div>';
endforeach;
?>
<div class="mb-4 border-bottom pb-3">
<label class="form-label fw-bold mb-2"><?php echo htmlspecialchars($user['username']); ?></label>
<input type="hidden" name="backpacks[<?php echo $user['id']; ?>]" id="input_bp_<?php echo $user['id']; ?>" value="<?php echo $my_current_bp_id; ?>">
<div class="d-flex align-items-center">
<!-- Selected Backpack Display -->
<div id="display_bp_<?php echo $user['id']; ?>" class="flex-grow-1 p-2 border rounded bg-white d-flex align-items-center" style="min-height: 60px;">
<?php if ($current_bp_details): ?>
<!-- Image if available -->
<?php if(!empty($current_bp_details['image_url'])): ?>
<img src="<?php echo htmlspecialchars($current_bp_details['image_url']); ?>" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" class="me-2">
<?php else: ?>
<i class="fas fa-hiking fa-2x text-muted me-2"></i>
<?php endif; ?>
<div>
<div class="fw-bold small"><?php echo htmlspecialchars($current_bp_details['name']); ?></div>
</div>
<?php else: ?>
<span class="text-muted small fst-italic">Kein Rucksack zugewiesen</span>
<?php endif; ?>
</div>
<button type="button" class="btn btn-sm btn-outline-primary ms-2" onclick="openBackpackModal(<?php echo $user['id']; ?>, '<?php echo htmlspecialchars($user['username']); ?>')">
<i class="fas fa-exchange-alt"></i>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
@@ -254,115 +225,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
</div>
</div>
<!-- Generic Modal -->
<div class="modal fade" id="genericBpModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="genericBpModalTitle">Rucksack wählen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body bg-light" id="genericBpModalBody">
<!-- Content loaded via JS -->
</div>
</div>
</div>
</div>
<script>
const userBackpacksData = <?php echo json_encode($user_backpacks_json); ?>;
const allAssignedIds = <?php echo json_encode($all_assigned_backpack_ids); ?>;
function openBackpackModal(userId, username) {
const data = userBackpacksData[userId];
const currentBpId = data.current_id;
const titleEl = document.getElementById('genericBpModalTitle');
const bodyEl = document.getElementById('genericBpModalBody');
titleEl.textContent = 'Rucksack für ' + username + ' wählen';
let html = '<div class="row g-3">';
// Option: None
html += `
<div class="col-md-4 col-6">
<div class="card h-100 text-center p-3 bp-select-card ${currentBpId == 0 ? 'border-primary bg-white' : ''}" onclick="selectBackpack(${userId}, 0)">
<div class="card-body">
<i class="fas fa-times-circle fa-2x text-muted mb-2"></i>
<div class="small fw-bold">Kein Rucksack</div>
</div>
</div>
</div>
`;
data.backpacks.forEach(bp => {
// Filter logic
const isAssigned = allAssignedIds.some(id => String(id) === String(bp.id));
if (isAssigned && String(bp.id) !== String(currentBpId)) return;
const imgHtml = bp.image_url ? `<img src="${bp.image_url}" class="card-img-top" style="height: 100px; object-fit: contain; padding: 10px;">` : `<div class="text-center py-4"><i class="fas fa-hiking fa-3x text-muted"></i></div>`;
const activeClass = (String(bp.id) === String(currentBpId)) ? 'border-primary bg-white' : '';
html += `
<div class="col-md-4 col-6">
<div class="card h-100 bp-select-card ${activeClass}" onclick='selectBackpack(${userId}, ${bp.id})'>
${imgHtml}
<div class="card-body p-2 text-center">
<div class="small fw-bold text-truncate">${bp.name}</div>
</div>
</div>
</div>
`;
});
html += '</div>';
bodyEl.innerHTML = html;
new bootstrap.Modal(document.getElementById('genericBpModal')).show();
}
function selectBackpack(userId, bpId) {
// Update Hidden Input
document.getElementById('input_bp_' + userId).value = bpId;
// Find BP Data for display
let bpName = 'Kein Rucksack zugewiesen';
let bpImage = null;
if (bpId > 0) {
const data = userBackpacksData[userId];
const bp = data.backpacks.find(b => String(b.id) === String(bpId));
if (bp) {
bpName = bp.name;
bpImage = bp.image_url;
}
}
// Update Display
const displayDiv = document.getElementById('display_bp_' + userId);
let html = '';
if (bpId == 0) {
html = '<span class="text-muted small fst-italic">Kein Rucksack zugewiesen</span>';
} else {
if (bpImage) {
html += '<img src="' + bpImage + '" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" class="me-2">';
} else {
html += '<i class="fas fa-hiking fa-2x text-muted me-2"></i>';
}
html += '<div><div class="fw-bold small">' + bpName + '</div></div>';
}
displayDiv.innerHTML = html;
// Close Modal
const modalEl = document.getElementById('genericBpModal');
const modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
}
</script>
<style>
.bp-select-card { cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; border: 1px solid rgba(0,0,0,0.1); }
.bp-select-card:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); border-color: var(--color-primary); }
</style>
<!-- Render Modal JS -->
<?php echo render_backpack_modal_script($user_backpacks_json, $all_assigned_backpack_ids); ?>
<?php require_once 'footer.php'; ?>

View File

@@ -161,6 +161,9 @@ function render_item_row($item, $level, $items_by_parent) {
$weight_display = $item['weight_grams'] > 0 ? number_format($item['weight_grams'], 0, ',', '.') . ' g' : '-';
$total_weight_display = $item['weight_grams'] > 0 ? number_format($item['weight_grams'] * $item['quantity'], 0, ',', '.') . ' g' : '-';
// Ensure children rows are initially shown or hidden based on some logic? Default shown.
// But the toggle button should reflect state. Default expanded -> chevron-down.
echo '<tr class="' . $bg_class . '" data-id="' . $item['id'] . '" data-parent-id="' . ($item['parent_packing_list_item_id'] ?: 0) . '">';
// Name Column with Indentation
@@ -169,9 +172,10 @@ function render_item_row($item, $level, $items_by_parent) {
// Tree Toggle or Spacer
if ($has_children) {
echo '<button class="btn btn-sm btn-link p-0 me-2 text-decoration-none toggle-tree-btn" data-target-id="' . $item['id'] . '"><i class="fas fa-chevron-down"></i></button>';
// Explicitly style the button
echo '<button type="button" class="btn btn-sm p-0 me-2 border-0 bg-transparent text-primary toggle-tree-btn" data-target-id="' . $item['id'] . '" style="width:20px; cursor:pointer;"><i class="fas fa-chevron-down"></i></button>';
} else {
echo '<span style="width: 20px; display: inline-block;"></span>';
echo '<span style="width: 20px; display: inline-block; margin-right: 0.5rem;"></span>';
}
echo $icon;