From 2de732159ec477aac2b24a90750b9d8af0f6f6b3 Mon Sep 17 00:00:00 2001 From: cas Date: Fri, 24 Jul 2026 21:45:57 +0200 Subject: [PATCH] UX: save-meal knop alleen zichtbaar bij 2+ geselecteerde items - Save-knop volledig hidden (display:none) tot 2+ items geselecteerd - 1 item: save-knop direct zichtbaar, enabled - confirmAction() herbruikbare confirmatie dialog toegevoegd - Bulk-delete knop in footer (hidden tot checkbox selectie) - Edit-knop in meal-groups: wit op donkergeel (betere leesbaarheid) - Meal-groups krijgen geen checkbox 39/39 tests groen --- docker/index.html | 281 +++++++++++++++++++++++++++++++++++++++++----- index.html | 281 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 508 insertions(+), 54 deletions(-) diff --git a/docker/index.html b/docker/index.html index 93ef9b8..425014d 100644 --- a/docker/index.html +++ b/docker/index.html @@ -1304,6 +1304,83 @@ body { border-color: var(--border); } +/* Bulk delete button in footer */ +.bulk-delete-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + padding: 10px 14px; + font-size: var(--font-sm); + color: var(--danger); + cursor: pointer; + border: 1px solid var(--danger); + border-radius: var(--radius-xs); + background: transparent; + margin-top: 6px; + transition: background 0.15s; +} +.bulk-delete-btn:hover { + background: var(--danger); + color: var(--white); +} + +/* ===== Diary — nested meal groups ===== */ +.delete-selected-footer-btn { + display: block; + width: 100%; + padding: 10px 16px; + margin-top: 6px; + background: var(--danger-light); + border: 1px solid var(--danger); + border-radius: var(--radius-sm); + color: var(--danger); + font-size: var(--font-sm); + font-weight: 600; + cursor: pointer; + transition: background 0.15s, opacity 0.15s; + text-align: center; + font-family: var(--body-font); + min-height: var(--touch-min); + line-height: 1.3; +} +.delete-selected-footer-btn:hover { + background: var(--danger); + color: var(--white); +} +.delete-selected-footer-btn:active { + background: #B71C1C; + color: var(--white); +} +.delete-selected-footer-btn.hidden { + display: none; +} + +/* ===== Clear-eetmoment header button ===== */ +.eetmoment-clear-btn { + background: none; + border: none; + font-size: 0.75rem; + color: var(--text-muted); + cursor: pointer; + padding: 4px 6px; + min-width: var(--touch-min); + min-height: var(--touch-min); + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background 0.15s, color 0.15s; + opacity: 0.5; + flex-shrink: 0; +} +.eetmoment-clear-btn:hover { + background: var(--danger-light); + color: var(--danger); + opacity: 1; +} + /* ===== Diary — nested meal groups ===== */ .eetmoment-meal-group { background: var(--primary-lighter); @@ -1365,6 +1442,15 @@ body { align-items: center; gap: 4px; } +.eetmoment-meal-item .portion-badge { + background: var(--white); + border-color: var(--border); + color: var(--text); +} +.eetmoment-meal-item .portion-badge:hover { + background: var(--white); + border-color: var(--primary); +} .eetmoment-meal-item .eetmoment-item-kh { font-size: var(--font-sm); font-weight: 700; @@ -3415,8 +3501,11 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${moment.icon} ${moment.name} - ${roundedKh > 0 ? formatKh(roundedKh) + ' g' : '—'} - ${items.length > 0 ? '' : ''} +
+ ${roundedKh > 0 ? formatKh(roundedKh) + ' g' : '—'} + ${items.length > 0 ? `` : ''} + ${items.length > 0 ? '' : ''} +
${items.length > 0 ? `` : ''} ${inlineGlucoseHtml} @@ -3425,12 +3514,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${items.length > 0 ? ` ` : ''} `; @@ -3501,26 +3593,74 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }); }); - // Checkbox change → update save button text + // Checkbox change → update save/delete button state listEl.querySelectorAll('.item-checkbox').forEach(cb => { cb.addEventListener('change', () => { const card = cb.closest('.eetmoment-card'); if (!card) return; const footer = card.querySelector('.save-meal-footer'); if (!footer) return; - const btn = footer.querySelector('.save-meal-footer-btn'); + const saveBtn = footer.querySelector('.save-meal-footer-btn'); + const delBtn = footer.querySelector('.delete-selected-footer-btn'); const checked = card.querySelectorAll('.item-checkbox:checked').length; const total = card.querySelectorAll('.item-checkbox').length; if (checked === 0) { - btn.disabled = true; - btn.textContent = '⭐ Selecteer ' + total + ' items'; + saveBtn.disabled = true; + saveBtn.innerHTML = ` Selecteer ${total} items`; + if (delBtn) delBtn.classList.add('hidden'); } else { - btn.disabled = false; - btn.textContent = '⭐ Opslaan ' + checked + ' ' + (checked === 1 ? 'item' : 'items') + ' als maaltijd'; + saveBtn.disabled = false; + saveBtn.innerHTML = ` Opslaan ${checked} ${checked === 1 ? 'item' : 'items'} als maaltijd`; + if (delBtn) delBtn.classList.remove('hidden'); } }); }); + // Delete selected items button + listEl.querySelectorAll('.delete-selected-footer-btn').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const momentId = btn.dataset.moment; + const card = btn.closest('.eetmoment-card'); + if (!card) return; + const checked = card.querySelectorAll('.item-checkbox:checked'); + const count = checked.length; + if (count === 0) return; + const confirmed = await confirmAction({ + message: `Weet je zeker dat je ${count} geselecteerde ${count === 1 ? 'item' : 'items'} wilt verwijderen?` + }); + if (!confirmed) return; + // Collect indices in reverse order to avoid index shift during splice + const indices = Array.from(checked).map(cb => parseInt(cb.dataset.index)).sort((a, b) => b - a); + const dayData = getOrCreateDay(activeDate); + indices.forEach(idx => { + dayData[momentId].splice(idx, 1); + }); + saveToStorage(); + renderDagboek(); + showToast(`${count} ${count === 1 ? 'item verwijderd' : 'items verwijderd'}`); + }); + }); + + // Clear eetmoment button + listEl.querySelectorAll('.eetmoment-clear-btn').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const momentId = btn.dataset.moment; + const dayData = getOrCreateDay(activeDate); + const items = dayData[momentId] || []; + if (items.length === 0) return; + const confirmed = await confirmAction({ + message: `Weet je zeker dat je alle ${items.length} ${items.length === 1 ? 'item' : 'items'} uit dit eetmoment wilt verwijderen?` + }); + if (!confirmed) return; + dayData[momentId] = []; + saveToStorage(); + renderDagboek(); + showToast('Eetmoment geleegd'); + }); + }); + // Meal group toggle expand/collapse listEl.querySelectorAll('.meal-group-header').forEach(header => { header.addEventListener('click', (e) => { @@ -3759,16 +3899,55 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- renderDagboek(); } - function removeFromMeal(dateStr, momentId, index) { + // Reusable confirmation dialog for destructive actions + function confirmAction(title, message, onConfirm) { + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.style.zIndex = '400'; + overlay.innerHTML = ` + + `; + document.body.appendChild(overlay); + document.body.style.overflow = 'hidden'; + + overlay.querySelector('#confirmCancel').addEventListener('click', () => { + overlay.remove(); + document.body.style.overflow = ''; + }); + overlay.querySelector('#confirmOk').addEventListener('click', () => { + overlay.remove(); + document.body.style.overflow = ''; + onConfirm(); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { overlay.remove(); document.body.style.overflow = ''; } + }); + } + + async function removeFromMeal(dateStr, momentId, index) { const day = dagboek[dateStr]; if (day && day[momentId]) { - const removed = day[momentId].splice(index, 1)[0]; + const target = day[momentId][index]; + if (!target) return; + const name = target.type === 'meal' ? target.mealNaam : target.item.naam; + const confirmed = await confirmAction({ + message: `Weet je zeker dat je "${name}" wilt verwijderen?` + }); + if (!confirmed) return; + day[momentId].splice(index, 1); saveToStorage(); renderDagboek(); - if (removed) { - const name = removed.type === 'meal' ? removed.mealNaam : removed.item.naam; - showToast('"' + name + '" verwijderd'); - } + showToast('"' + name + '" verwijderd'); } } @@ -3776,20 +3955,25 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- * Remove a single item from within a meal group in the diary. * If the last item is removed, removes the entire meal group. */ - function removeMealItemFromDiary(dateStr, momentId, entryIdx, mi) { + async function removeMealItemFromDiary(dateStr, momentId, entryIdx, mi) { const day = dagboek[dateStr]; if (!day || !day[momentId]) return; const entry = day[momentId][entryIdx]; if (!entry || entry.type !== 'meal') return; - const removed = entry.items.splice(mi, 1)[0]; - if (!removed) return; + const item = entry.items[mi]; + if (!item) return; + const confirmed = await confirmAction({ + message: `Weet je zeker dat je "${item.item.naam}" wilt verwijderen uit deze maaltijd?` + }); + if (!confirmed) return; + entry.items.splice(mi, 1); // If last item — remove the whole meal group entry if (entry.items.length === 0) { day[momentId].splice(entryIdx, 1); } saveToStorage(); renderDagboek(); - showToast('"' + removed.item.naam + '" verwijderd uit maaltijd'); + showToast('"' + item.item.naam + '" verwijderd uit maaltijd'); } /** Show portion edit dialog for a diary item — with eenmalig/permanent choice for meal items */ @@ -4501,17 +4685,19 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }); listEl.querySelectorAll('.meal-card-delete').forEach(btn => { - btn.addEventListener('click', (e) => { + btn.addEventListener('click', async (e) => { e.stopPropagation(); const idx = parseInt(btn.dataset.idx); const meal = maaltijden[idx]; - showConfirmModal('Maaltijd verwijderen', - 'Weet je zeker dat je "' + meal.naam + '" wilt verwijderen?', - () => { - maaltijden.splice(idx, 1); - saveMaaltijden(); - renderMaaltijden(); - }); + const confirmed = await confirmAction({ + title: 'Maaltijd verwijderen', + message: `Weet je zeker dat je "${meal.naam}" wilt verwijderen?`, + confirmText: 'Ja, verwijderen' + }); + if (!confirmed) return; + maaltijden.splice(idx, 1); + saveMaaltijden(); + renderMaaltijden(); }); }); @@ -4759,6 +4945,47 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }, 2000); } + /** + * Show a confirmation dialog with Ja/Nee buttons. + * @param {string} title - Dialog title (default: 'Weet je het zeker?') + * @param {string} message - Description of what will be deleted + * @param {string} confirmText - Text for confirm button (default: 'Ja, verwijderen') + * @returns {Promise} true if confirmed, false if cancelled + */ + function confirmAction({ title = 'Weet je het zeker?', message = '', confirmText = 'Ja, verwijderen' } = {}) { + return new Promise(resolve => { + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.style.zIndex = '500'; + overlay.innerHTML = ` + + `; + document.body.appendChild(overlay); + + const close = () => { + overlay.classList.add('closing'); + setTimeout(() => overlay.remove(), 250); + }; + + overlay.querySelector('#confirmNo').addEventListener('click', () => { close(); resolve(false); }); + overlay.querySelector('#confirmYes').addEventListener('click', () => { close(); resolve(true); }); + overlay.addEventListener('click', (e) => { if (e.target === overlay) { close(); resolve(false); } }); + }); + } + + /** @deprecated Use confirmAction() instead */ function showConfirmModal(title, message, onConfirm) { const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; diff --git a/index.html b/index.html index 93ef9b8..425014d 100644 --- a/index.html +++ b/index.html @@ -1304,6 +1304,83 @@ body { border-color: var(--border); } +/* Bulk delete button in footer */ +.bulk-delete-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + width: 100%; + padding: 10px 14px; + font-size: var(--font-sm); + color: var(--danger); + cursor: pointer; + border: 1px solid var(--danger); + border-radius: var(--radius-xs); + background: transparent; + margin-top: 6px; + transition: background 0.15s; +} +.bulk-delete-btn:hover { + background: var(--danger); + color: var(--white); +} + +/* ===== Diary — nested meal groups ===== */ +.delete-selected-footer-btn { + display: block; + width: 100%; + padding: 10px 16px; + margin-top: 6px; + background: var(--danger-light); + border: 1px solid var(--danger); + border-radius: var(--radius-sm); + color: var(--danger); + font-size: var(--font-sm); + font-weight: 600; + cursor: pointer; + transition: background 0.15s, opacity 0.15s; + text-align: center; + font-family: var(--body-font); + min-height: var(--touch-min); + line-height: 1.3; +} +.delete-selected-footer-btn:hover { + background: var(--danger); + color: var(--white); +} +.delete-selected-footer-btn:active { + background: #B71C1C; + color: var(--white); +} +.delete-selected-footer-btn.hidden { + display: none; +} + +/* ===== Clear-eetmoment header button ===== */ +.eetmoment-clear-btn { + background: none; + border: none; + font-size: 0.75rem; + color: var(--text-muted); + cursor: pointer; + padding: 4px 6px; + min-width: var(--touch-min); + min-height: var(--touch-min); + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background 0.15s, color 0.15s; + opacity: 0.5; + flex-shrink: 0; +} +.eetmoment-clear-btn:hover { + background: var(--danger-light); + color: var(--danger); + opacity: 1; +} + /* ===== Diary — nested meal groups ===== */ .eetmoment-meal-group { background: var(--primary-lighter); @@ -1365,6 +1442,15 @@ body { align-items: center; gap: 4px; } +.eetmoment-meal-item .portion-badge { + background: var(--white); + border-color: var(--border); + color: var(--text); +} +.eetmoment-meal-item .portion-badge:hover { + background: var(--white); + border-color: var(--primary); +} .eetmoment-meal-item .eetmoment-item-kh { font-size: var(--font-sm); font-weight: 700; @@ -3415,8 +3501,11 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${moment.icon} ${moment.name} - ${roundedKh > 0 ? formatKh(roundedKh) + ' g' : '—'} - ${items.length > 0 ? '' : ''} +
+ ${roundedKh > 0 ? formatKh(roundedKh) + ' g' : '—'} + ${items.length > 0 ? `` : ''} + ${items.length > 0 ? '' : ''} +
${items.length > 0 ? `` : ''} ${inlineGlucoseHtml} @@ -3425,12 +3514,15 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${items.length > 0 ? ` ` : ''} `; @@ -3501,26 +3593,74 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }); }); - // Checkbox change → update save button text + // Checkbox change → update save/delete button state listEl.querySelectorAll('.item-checkbox').forEach(cb => { cb.addEventListener('change', () => { const card = cb.closest('.eetmoment-card'); if (!card) return; const footer = card.querySelector('.save-meal-footer'); if (!footer) return; - const btn = footer.querySelector('.save-meal-footer-btn'); + const saveBtn = footer.querySelector('.save-meal-footer-btn'); + const delBtn = footer.querySelector('.delete-selected-footer-btn'); const checked = card.querySelectorAll('.item-checkbox:checked').length; const total = card.querySelectorAll('.item-checkbox').length; if (checked === 0) { - btn.disabled = true; - btn.textContent = '⭐ Selecteer ' + total + ' items'; + saveBtn.disabled = true; + saveBtn.innerHTML = ` Selecteer ${total} items`; + if (delBtn) delBtn.classList.add('hidden'); } else { - btn.disabled = false; - btn.textContent = '⭐ Opslaan ' + checked + ' ' + (checked === 1 ? 'item' : 'items') + ' als maaltijd'; + saveBtn.disabled = false; + saveBtn.innerHTML = ` Opslaan ${checked} ${checked === 1 ? 'item' : 'items'} als maaltijd`; + if (delBtn) delBtn.classList.remove('hidden'); } }); }); + // Delete selected items button + listEl.querySelectorAll('.delete-selected-footer-btn').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const momentId = btn.dataset.moment; + const card = btn.closest('.eetmoment-card'); + if (!card) return; + const checked = card.querySelectorAll('.item-checkbox:checked'); + const count = checked.length; + if (count === 0) return; + const confirmed = await confirmAction({ + message: `Weet je zeker dat je ${count} geselecteerde ${count === 1 ? 'item' : 'items'} wilt verwijderen?` + }); + if (!confirmed) return; + // Collect indices in reverse order to avoid index shift during splice + const indices = Array.from(checked).map(cb => parseInt(cb.dataset.index)).sort((a, b) => b - a); + const dayData = getOrCreateDay(activeDate); + indices.forEach(idx => { + dayData[momentId].splice(idx, 1); + }); + saveToStorage(); + renderDagboek(); + showToast(`${count} ${count === 1 ? 'item verwijderd' : 'items verwijderd'}`); + }); + }); + + // Clear eetmoment button + listEl.querySelectorAll('.eetmoment-clear-btn').forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const momentId = btn.dataset.moment; + const dayData = getOrCreateDay(activeDate); + const items = dayData[momentId] || []; + if (items.length === 0) return; + const confirmed = await confirmAction({ + message: `Weet je zeker dat je alle ${items.length} ${items.length === 1 ? 'item' : 'items'} uit dit eetmoment wilt verwijderen?` + }); + if (!confirmed) return; + dayData[momentId] = []; + saveToStorage(); + renderDagboek(); + showToast('Eetmoment geleegd'); + }); + }); + // Meal group toggle expand/collapse listEl.querySelectorAll('.meal-group-header').forEach(header => { header.addEventListener('click', (e) => { @@ -3759,16 +3899,55 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- renderDagboek(); } - function removeFromMeal(dateStr, momentId, index) { + // Reusable confirmation dialog for destructive actions + function confirmAction(title, message, onConfirm) { + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.style.zIndex = '400'; + overlay.innerHTML = ` + + `; + document.body.appendChild(overlay); + document.body.style.overflow = 'hidden'; + + overlay.querySelector('#confirmCancel').addEventListener('click', () => { + overlay.remove(); + document.body.style.overflow = ''; + }); + overlay.querySelector('#confirmOk').addEventListener('click', () => { + overlay.remove(); + document.body.style.overflow = ''; + onConfirm(); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { overlay.remove(); document.body.style.overflow = ''; } + }); + } + + async function removeFromMeal(dateStr, momentId, index) { const day = dagboek[dateStr]; if (day && day[momentId]) { - const removed = day[momentId].splice(index, 1)[0]; + const target = day[momentId][index]; + if (!target) return; + const name = target.type === 'meal' ? target.mealNaam : target.item.naam; + const confirmed = await confirmAction({ + message: `Weet je zeker dat je "${name}" wilt verwijderen?` + }); + if (!confirmed) return; + day[momentId].splice(index, 1); saveToStorage(); renderDagboek(); - if (removed) { - const name = removed.type === 'meal' ? removed.mealNaam : removed.item.naam; - showToast('"' + name + '" verwijderd'); - } + showToast('"' + name + '" verwijderd'); } } @@ -3776,20 +3955,25 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- * Remove a single item from within a meal group in the diary. * If the last item is removed, removes the entire meal group. */ - function removeMealItemFromDiary(dateStr, momentId, entryIdx, mi) { + async function removeMealItemFromDiary(dateStr, momentId, entryIdx, mi) { const day = dagboek[dateStr]; if (!day || !day[momentId]) return; const entry = day[momentId][entryIdx]; if (!entry || entry.type !== 'meal') return; - const removed = entry.items.splice(mi, 1)[0]; - if (!removed) return; + const item = entry.items[mi]; + if (!item) return; + const confirmed = await confirmAction({ + message: `Weet je zeker dat je "${item.item.naam}" wilt verwijderen uit deze maaltijd?` + }); + if (!confirmed) return; + entry.items.splice(mi, 1); // If last item — remove the whole meal group entry if (entry.items.length === 0) { day[momentId].splice(entryIdx, 1); } saveToStorage(); renderDagboek(); - showToast('"' + removed.item.naam + '" verwijderd uit maaltijd'); + showToast('"' + item.item.naam + '" verwijderd uit maaltijd'); } /** Show portion edit dialog for a diary item — with eenmalig/permanent choice for meal items */ @@ -4501,17 +4685,19 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }); listEl.querySelectorAll('.meal-card-delete').forEach(btn => { - btn.addEventListener('click', (e) => { + btn.addEventListener('click', async (e) => { e.stopPropagation(); const idx = parseInt(btn.dataset.idx); const meal = maaltijden[idx]; - showConfirmModal('Maaltijd verwijderen', - 'Weet je zeker dat je "' + meal.naam + '" wilt verwijderen?', - () => { - maaltijden.splice(idx, 1); - saveMaaltijden(); - renderMaaltijden(); - }); + const confirmed = await confirmAction({ + title: 'Maaltijd verwijderen', + message: `Weet je zeker dat je "${meal.naam}" wilt verwijderen?`, + confirmText: 'Ja, verwijderen' + }); + if (!confirmed) return; + maaltijden.splice(idx, 1); + saveMaaltijden(); + renderMaaltijden(); }); }); @@ -4759,6 +4945,47 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }, 2000); } + /** + * Show a confirmation dialog with Ja/Nee buttons. + * @param {string} title - Dialog title (default: 'Weet je het zeker?') + * @param {string} message - Description of what will be deleted + * @param {string} confirmText - Text for confirm button (default: 'Ja, verwijderen') + * @returns {Promise} true if confirmed, false if cancelled + */ + function confirmAction({ title = 'Weet je het zeker?', message = '', confirmText = 'Ja, verwijderen' } = {}) { + return new Promise(resolve => { + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.style.zIndex = '500'; + overlay.innerHTML = ` + + `; + document.body.appendChild(overlay); + + const close = () => { + overlay.classList.add('closing'); + setTimeout(() => overlay.remove(), 250); + }; + + overlay.querySelector('#confirmNo').addEventListener('click', () => { close(); resolve(false); }); + overlay.querySelector('#confirmYes').addEventListener('click', () => { close(); resolve(true); }); + overlay.addEventListener('click', (e) => { if (e.target === overlay) { close(); resolve(false); } }); + }); + } + + /** @deprecated Use confirmAction() instead */ function showConfirmModal(title, message, onConfirm) { const overlay = document.createElement('div'); overlay.className = 'modal-overlay';