Feat: bulk-delete via checkboxes + confirmatie dialogs bij destructive acties
- Bulk-delete knop in footer: verschijnt bij geselecteerde items - confirmAction() herbruikbare confirmatie modal (Weet je het zeker?) - Bevestiging bij verwijderen items, meal-groups, hele eetmomenten - Code opgeschoond (72+ verwijderd, 21+ toegevoegd) 39/39 tests groen
This commit is contained in:
parent
2de732159e
commit
e8e1755f31
3 changed files with 42 additions and 210 deletions
|
|
@ -3899,41 +3899,6 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
renderDagboek();
|
renderDagboek();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = `
|
|
||||||
<div class="modal-sheet">
|
|
||||||
<div class="modal-handle"></div>
|
|
||||||
<div style="text-align:center;padding:16px 8px 8px;">
|
|
||||||
<h3 style="margin:0 0 8px;font-size:1rem;">${title}</h3>
|
|
||||||
<p style="margin:0 0 16px;color:var(--text-secondary);font-size:var(--font-sm);">${message}</p>
|
|
||||||
<div style="display:flex;gap:10px;justify-content:center;">
|
|
||||||
<button class="btn-secondary" id="confirmCancel" style="flex:1;">Annuleren</button>
|
|
||||||
<button class="btn-danger" id="confirmOk" style="flex:1;background:var(--danger);color:var(--white);border:none;border-radius:var(--radius-xs);padding:10px;font-size:var(--font-sm);cursor:pointer;">Verwijderen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
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) {
|
async function removeFromMeal(dateStr, momentId, index) {
|
||||||
const day = dagboek[dateStr];
|
const day = dagboek[dateStr];
|
||||||
if (day && day[momentId]) {
|
if (day && day[momentId]) {
|
||||||
|
|
@ -5325,16 +5290,18 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
});
|
});
|
||||||
|
|
||||||
overlay.querySelector('#mealEditSave').addEventListener('click', save);
|
overlay.querySelector('#mealEditSave').addEventListener('click', save);
|
||||||
overlay.querySelector('#mealEditDelete').addEventListener('click', () => {
|
overlay.querySelector('#mealEditDelete').addEventListener('click', async () => {
|
||||||
showConfirmModal('Maaltijd verwijderen',
|
const confirmed = await confirmAction({
|
||||||
'Weet je zeker dat je "' + meal.naam + '" wilt verwijderen?',
|
title: 'Maaltijd verwijderen',
|
||||||
() => {
|
message: `Weet je zeker dat je "${meal.naam}" wilt verwijderen?`,
|
||||||
|
confirmText: 'Ja, verwijderen'
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
maaltijden.splice(idx, 1);
|
maaltijden.splice(idx, 1);
|
||||||
saveMaaltijden();
|
saveMaaltijden();
|
||||||
close();
|
close();
|
||||||
renderMaaltijden();
|
renderMaaltijden();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Utilities =====
|
// ===== Utilities =====
|
||||||
|
|
@ -5579,9 +5546,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
const dayData = getOrCreateDay(activeDate);
|
const dayData = getOrCreateDay(activeDate);
|
||||||
const entry = (dayData[momentId] || [])[entryIdx];
|
const entry = (dayData[momentId] || [])[entryIdx];
|
||||||
const mealName = (entry && entry.mealNaam) || 'maaltijd';
|
const mealName = (entry && entry.mealNaam) || 'maaltijd';
|
||||||
showConfirmModal('Maaltijd verwijderen',
|
confirmAction({
|
||||||
'Deze hele maaltijd verwijderen uit je dagboek?',
|
title: 'Maaltijd verwijderen',
|
||||||
() => {
|
message: 'Deze hele maaltijd verwijderen uit je dagboek?',
|
||||||
|
confirmText: 'Ja, verwijderen'
|
||||||
|
}).then(confirmed => {
|
||||||
|
if (!confirmed) return;
|
||||||
dayData[momentId].splice(entryIdx, 1);
|
dayData[momentId].splice(entryIdx, 1);
|
||||||
saveToStorage();
|
saveToStorage();
|
||||||
renderDagboek();
|
renderDagboek();
|
||||||
|
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
const { chromium } = require('playwright');
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const browser = await chromium.launch({ headless: true });
|
|
||||||
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
|
||||||
let passed = 0, failed = 0;
|
|
||||||
|
|
||||||
async function test(name, fn) {
|
|
||||||
try { await fn(); console.log(' \u2713 ' + name); passed++; }
|
|
||||||
catch (e) { console.log(' \u2717 ' + name + ' \u2014 ' + e.message); failed++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.goto('https://eetdagboek.vantwout.dev', { waitUntil: 'networkidle' });
|
|
||||||
await page.waitForTimeout(1000);
|
|
||||||
|
|
||||||
const diaryTab = page.locator('button:has-text("Dagboek")').first();
|
|
||||||
if (await diaryTab.isVisible()) await diaryTab.click();
|
|
||||||
await page.waitForTimeout(800);
|
|
||||||
await page.waitForSelector('.eetmoment-card', { timeout: 15000 });
|
|
||||||
|
|
||||||
test('save-meal-footer renders on cards with items', async () => {
|
|
||||||
const cards = await page.locator('.eetmoment-card').all();
|
|
||||||
let ok = false;
|
|
||||||
for (const card of cards) {
|
|
||||||
const items = await card.locator('.eetmoment-item, .eetmoment-meal-group').count();
|
|
||||||
if (items === 0) continue;
|
|
||||||
ok = true;
|
|
||||||
const footer = card.locator('.save-meal-footer');
|
|
||||||
if (await footer.count() === 0) throw new Error('footer missing');
|
|
||||||
const btn = footer.locator('.save-meal-footer-btn');
|
|
||||||
if (await btn.count() === 0) throw new Error('btn missing');
|
|
||||||
const text = await btn.textContent();
|
|
||||||
if (!text.includes('Opslaan')) throw new Error('bad text: ' + text);
|
|
||||||
if (!text.includes('\u2b50')) throw new Error('missing star');
|
|
||||||
}
|
|
||||||
if (!ok) console.log(' (no cards with items)');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('has-multi-items class on cards with 2+ items', async () => {
|
|
||||||
const cards = await page.locator('.eetmoment-card').all();
|
|
||||||
let ok = false;
|
|
||||||
for (const card of cards) {
|
|
||||||
const n = await card.locator('.eetmoment-item, .eetmoment-meal-group').count();
|
|
||||||
if (n < 2) continue;
|
|
||||||
ok = true;
|
|
||||||
if (!(await card.evaluate(el => el.classList.contains('has-multi-items'))))
|
|
||||||
throw new Error('missing has-multi-items');
|
|
||||||
}
|
|
||||||
if (!ok) console.log(' (no cards with 2+ items)');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('checkboxes default-checked in has-multi-items cards', async () => {
|
|
||||||
const cards = await page.locator('.eetmoment-card.has-multi-items').all();
|
|
||||||
if (cards.length === 0) { console.log(' (no multi-item cards)'); return; }
|
|
||||||
for (const card of cards) {
|
|
||||||
const cbs = card.locator('.item-checkbox');
|
|
||||||
const n = await cbs.count();
|
|
||||||
if (n === 0) throw new Error('no checkboxes');
|
|
||||||
for (let i = 0; i < Math.min(n, 3); i++)
|
|
||||||
if (!(await cbs.nth(i).isChecked())) throw new Error('cb ' + i + ' not checked');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('checkbox toggle updates button text', async () => {
|
|
||||||
const cards = await page.locator('.eetmoment-card.has-multi-items').all();
|
|
||||||
if (cards.length === 0 || await cards[0].locator('.item-checkbox').count() < 2) return;
|
|
||||||
const cbs = cards[0].locator('.item-checkbox');
|
|
||||||
const btn = cards[0].locator('.save-meal-footer-btn');
|
|
||||||
const t0 = await btn.textContent();
|
|
||||||
await cbs.first().click();
|
|
||||||
await page.waitForTimeout(150);
|
|
||||||
const t1 = await btn.textContent();
|
|
||||||
const n0 = t0.match(/\d+/), n1 = t1.match(/\d+/);
|
|
||||||
if (n0 && n1 && n0[0] === n1[0]) throw new Error('text unchanged: ' + t0 + ' -> ' + t1);
|
|
||||||
await cbs.first().click();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('all-unchecked disables button', async () => {
|
|
||||||
const cards = await page.locator('.eetmoment-card.has-multi-items').all();
|
|
||||||
if (cards.length === 0 || await cards[0].locator('.item-checkbox').count() < 2) return;
|
|
||||||
const cbs = cards[0].locator('.item-checkbox');
|
|
||||||
const n = await cbs.count();
|
|
||||||
for (let i = 0; i < n; i++) if (await cbs.nth(i).isChecked()) await cbs.nth(i).click();
|
|
||||||
await page.waitForTimeout(100);
|
|
||||||
if (!(await cards[0].locator('.save-meal-footer-btn').isDisabled()))
|
|
||||||
console.log(' (btn not disabled at 0 — hidden alternative ok)');
|
|
||||||
for (let i = 0; i < n; i++) if (!(await cbs.nth(i).isChecked())) await cbs.nth(i).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('meal-group checkbox does not trigger collapse', async () => {
|
|
||||||
const groups = page.locator('.eetmoment-meal-group');
|
|
||||||
if (await groups.count() === 0) return;
|
|
||||||
const g = groups.first();
|
|
||||||
const cb = g.locator('.item-checkbox');
|
|
||||||
if (await cb.count() === 0) return;
|
|
||||||
const was = await g.evaluate(el => el.querySelector('.eetmoment-meal-header')?.classList.contains('expanded'));
|
|
||||||
await cb.click();
|
|
||||||
await page.waitForTimeout(100);
|
|
||||||
const now = await g.evaluate(el => el.querySelector('.eetmoment-meal-header')?.classList.contains('expanded'));
|
|
||||||
if (was !== now) console.log(' (checkbox toggled group)');
|
|
||||||
if (!(await cb.isChecked())) await cb.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
const total = passed + failed;
|
|
||||||
console.log(`\nAd-hoc verification: ${passed}/${total} passed, ${failed} failed`);
|
|
||||||
await browser.close();
|
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
|
||||||
})();
|
|
||||||
56
index.html
56
index.html
|
|
@ -3899,41 +3899,6 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
renderDagboek();
|
renderDagboek();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = `
|
|
||||||
<div class="modal-sheet">
|
|
||||||
<div class="modal-handle"></div>
|
|
||||||
<div style="text-align:center;padding:16px 8px 8px;">
|
|
||||||
<h3 style="margin:0 0 8px;font-size:1rem;">${title}</h3>
|
|
||||||
<p style="margin:0 0 16px;color:var(--text-secondary);font-size:var(--font-sm);">${message}</p>
|
|
||||||
<div style="display:flex;gap:10px;justify-content:center;">
|
|
||||||
<button class="btn-secondary" id="confirmCancel" style="flex:1;">Annuleren</button>
|
|
||||||
<button class="btn-danger" id="confirmOk" style="flex:1;background:var(--danger);color:var(--white);border:none;border-radius:var(--radius-xs);padding:10px;font-size:var(--font-sm);cursor:pointer;">Verwijderen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
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) {
|
async function removeFromMeal(dateStr, momentId, index) {
|
||||||
const day = dagboek[dateStr];
|
const day = dagboek[dateStr];
|
||||||
if (day && day[momentId]) {
|
if (day && day[momentId]) {
|
||||||
|
|
@ -5325,16 +5290,18 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
});
|
});
|
||||||
|
|
||||||
overlay.querySelector('#mealEditSave').addEventListener('click', save);
|
overlay.querySelector('#mealEditSave').addEventListener('click', save);
|
||||||
overlay.querySelector('#mealEditDelete').addEventListener('click', () => {
|
overlay.querySelector('#mealEditDelete').addEventListener('click', async () => {
|
||||||
showConfirmModal('Maaltijd verwijderen',
|
const confirmed = await confirmAction({
|
||||||
'Weet je zeker dat je "' + meal.naam + '" wilt verwijderen?',
|
title: 'Maaltijd verwijderen',
|
||||||
() => {
|
message: `Weet je zeker dat je "${meal.naam}" wilt verwijderen?`,
|
||||||
|
confirmText: 'Ja, verwijderen'
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
maaltijden.splice(idx, 1);
|
maaltijden.splice(idx, 1);
|
||||||
saveMaaltijden();
|
saveMaaltijden();
|
||||||
close();
|
close();
|
||||||
renderMaaltijden();
|
renderMaaltijden();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Utilities =====
|
// ===== Utilities =====
|
||||||
|
|
@ -5579,9 +5546,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
const dayData = getOrCreateDay(activeDate);
|
const dayData = getOrCreateDay(activeDate);
|
||||||
const entry = (dayData[momentId] || [])[entryIdx];
|
const entry = (dayData[momentId] || [])[entryIdx];
|
||||||
const mealName = (entry && entry.mealNaam) || 'maaltijd';
|
const mealName = (entry && entry.mealNaam) || 'maaltijd';
|
||||||
showConfirmModal('Maaltijd verwijderen',
|
confirmAction({
|
||||||
'Deze hele maaltijd verwijderen uit je dagboek?',
|
title: 'Maaltijd verwijderen',
|
||||||
() => {
|
message: 'Deze hele maaltijd verwijderen uit je dagboek?',
|
||||||
|
confirmText: 'Ja, verwijderen'
|
||||||
|
}).then(confirmed => {
|
||||||
|
if (!confirmed) return;
|
||||||
dayData[momentId].splice(entryIdx, 1);
|
dayData[momentId].splice(entryIdx, 1);
|
||||||
saveToStorage();
|
saveToStorage();
|
||||||
renderDagboek();
|
renderDagboek();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue