diff --git a/docker/index.html b/docker/index.html index 33b64d6..617473d 100644 --- a/docker/index.html +++ b/docker/index.html @@ -465,96 +465,111 @@ body { background: var(--progress-red); } -/* ===== Glucose Section ===== */ -.glucose-section { +/* ===== Nuchter card — compact glucose card vóór Ontbijt ===== */ +.nuchter-card { background: var(--white); border-radius: var(--radius); box-shadow: var(--shadow-sm); - padding: 14px 16px; + overflow: hidden; margin-bottom: 10px; -} - -.glucose-header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 12px; -} - -.glucose-icon { - font-size: 1.1rem; - color: var(--progress-red); -} - -.glucose-title { - font-weight: 700; - font-size: var(--font-md); - color: var(--text); -} - -.glucose-unit-label { - font-size: var(--font-xs); - color: var(--text-muted); - margin-left: 2px; -} - -/* Grid: 4 columns on wide, 2×2 on narrow */ -.glucose-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; -} - -@media (max-width: 480px) { - .glucose-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -.glucose-item { - background: var(--bg); - border-radius: var(--radius-sm); - padding: 10px 8px; - text-align: center; border: 1px solid var(--border); transition: border-color 0.2s, background 0.2s; + list-style: none; } - -.glucose-item.glucose-good { +.nuchter-card.glucose-good { border-color: #28a745; background: #f0fdf4; } - -.glucose-item.glucose-bad { +.nuchter-card.glucose-bad { border-color: var(--progress-red); background: #fef2f2; } - -.glucose-item.glucose-empty { +.nuchter-card.glucose-empty { border-color: var(--border); - background: var(--bg); + background: var(--white); +} +.nuchter-card-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: var(--primary-lighter); +} +.nuchter-card-icon { + font-size: 1.1rem; +} +.nuchter-card-label { + font-weight: 600; + font-size: var(--font-sm); + color: var(--text); +} +.nuchter-card-range { + font-size: var(--font-xs); + color: var(--text-muted); + margin-left: auto; +} +.nuchter-card-body { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + gap: 12px; +} +.nuchter-card-body .glucose-input-wrapper { + margin-bottom: 0; } -.glucose-item-label { +/* ===== Inline glucose in eetmoment card ===== */ +.glucose-inline { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 14px; + border-top: 1px solid #F0F0F0; + gap: 8px; + transition: background 0.15s; +} +.glucose-inline.glucose-good { + background: #f0fdf4; +} +.glucose-inline.glucose-bad { + background: #fef2f2; +} +.glucose-inline.glucose-empty { + background: transparent; +} +.glucose-inline-left { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} +.glucose-inline-icon { + font-size: 0.9rem; + color: var(--progress-red); +} +.glucose-inline-label { font-size: var(--font-xs); font-weight: 600; color: var(--text-secondary); - margin-bottom: 2px; - line-height: 1.3; } - -.glucose-item-range { +.glucose-inline-range { font-size: 0.65rem; color: var(--text-muted); - margin-bottom: 8px; } +.glucose-inline-right { + display: flex; + align-items: center; + gap: 8px; +} + +/* ===== Shared glucose input styles ===== */ .glucose-input-wrapper { display: flex; align-items: center; justify-content: center; gap: 2px; - margin-bottom: 6px; } .glucose-input { @@ -2258,15 +2273,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
- -
-
- - Glucose - mmol/L -
-
-
+ @@ -2455,10 +2462,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // ===== Prikmomenten voor glucose ===== const PRICKMOMENTEN = [ - { key: 'nuchter', label: 'Nuchter (voor ontbijt)', targetMin: 0, targetMax: 5.4 }, - { key: 'naOntbijt', label: '2 uur na ontbijt', targetMin: 5.4, targetMax: 6.7 }, - { key: 'naLunch', label: '2 uur na lunch', targetMin: 5.4, targetMax: 6.7 }, - { key: 'naAvondeten', label: '2 uur na avondeten', targetMin: 5.4, targetMax: 6.7 } + { key: 'nuchter', label: 'Nuchter (voor ontbijt)', targetMin: 0, targetMax: 5.4, momentId: null }, + { key: 'naOntbijt', label: '2 uur na ontbijt', targetMin: 5.4, targetMax: 6.7, momentId: 'Ontbijt' }, + { key: 'naLunch', label: '2 uur na lunch', targetMin: 5.4, targetMax: 6.7, momentId: 'Lunch' }, + { key: 'naAvondeten', label: '2 uur na avondeten', targetMin: 5.4, targetMax: 6.7, momentId: 'Avondeten' } ]; // ===== Standaard portiegroottes per categorie ===== @@ -2704,48 +2711,102 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } } - function renderGlucoseSection() { - const grid = document.getElementById('glucoseGrid'); - if (!grid) return; + // ===== Glucose — Inline per eetmoment ===== + function renderNuchterCard() { + const m = PRICKMOMENTEN.find(p => p.key === 'nuchter'); + if (!m) return ''; ensureGlucoseDay(activeDate); + const value = glucose[activeDate]?.['nuchter'] ?? null; + const status = isGlucoseInRange(value, 'nuchter'); - grid.innerHTML = PRICKMOMENTEN.map(m => { - const value = glucose[activeDate][m.key]; - const status = isGlucoseInRange(value, m.key); + let statusIcon, statusClass, statusText; + if (value === null || value === undefined || value === '') { + statusIcon = '⏹️'; + statusClass = 'glucose-empty'; + statusText = 'Nog meten'; + } else if (status === 'good') { + statusIcon = '✅'; + statusClass = 'glucose-good'; + statusText = 'Binnen range'; + } else if (status === 'high') { + statusIcon = '🔴'; + statusClass = 'glucose-bad'; + statusText = 'Te hoog'; + } else { + statusIcon = '🔴'; + statusClass = 'glucose-bad'; + statusText = 'Te laag'; + } - let statusIcon, statusClass, statusText; - if (value === null || value === undefined || value === '') { - statusIcon = '⏹️'; - statusClass = 'glucose-empty'; - statusText = 'Nog meten'; - } else if (status === 'good') { - statusIcon = '✅'; - statusClass = 'glucose-good'; - statusText = 'Binnen range'; - } else if (status === 'high') { - statusIcon = '🔴'; - statusClass = 'glucose-bad'; - statusText = 'Te hoog'; - } else { - statusIcon = '🔴'; - statusClass = 'glucose-bad'; - statusText = 'Te laag'; - } + const displayValue = (value !== null && value !== undefined) ? value : ''; - const displayValue = (value !== null && value !== undefined) ? value : ''; + return ` +
  • +
    + 🩸 + Nuchter (voor ontbijt) + < ${m.targetMax} +
    +
    +
    + + mmol/L +
    +
    + ${statusIcon} + ${statusText} +
    +
    +
  • + `; + } - // Short label: remove parenthetical and "2 uur na" prefix - let shortLabel = m.label; - shortLabel = shortLabel.replace(/\([^)]*\)/g, '').trim(); - shortLabel = shortLabel.replace(/^2 uur na /i, '').trim(); - // Capitalize first letter - shortLabel = shortLabel.charAt(0).toUpperCase() + shortLabel.slice(1); + function renderInlineGlucoseItem(m) { + ensureGlucoseDay(activeDate); + const value = glucose[activeDate]?.[m.key] ?? null; + const status = isGlucoseInRange(value, m.key); - return ` -
    -
    ${shortLabel}
    -
    ${m.key === 'nuchter' ? '< ' + m.targetMax : m.targetMin + '–' + m.targetMax}
    + let statusIcon, statusClass, statusText; + if (value === null || value === undefined || value === '') { + statusIcon = '⏹️'; + statusClass = 'glucose-empty'; + statusText = 'Nog meten'; + } else if (status === 'good') { + statusIcon = '✅'; + statusClass = 'glucose-good'; + statusText = 'Binnen range'; + } else if (status === 'high') { + statusIcon = '🔴'; + statusClass = 'glucose-bad'; + statusText = 'Te hoog'; + } else { + statusIcon = '🔴'; + statusClass = 'glucose-bad'; + statusText = 'Te laag'; + } + + const displayValue = (value !== null && value !== undefined) ? value : ''; + + // Short label: remove parenthetical and "2 uur na" prefix + let shortLabel = m.label; + shortLabel = shortLabel.replace(/\([^)]*\)/g, '').trim(); + shortLabel = shortLabel.replace(/^2 uur na /i, '').trim(); + shortLabel = shortLabel.charAt(0).toUpperCase() + shortLabel.slice(1); + + return ` +
    +
    + 🩸 + ${shortLabel} + ${m.targetMin}–${m.targetMax} +
    +
    ${statusText}
    - `; - }).join(''); +
    + `; + } + + function attachGlucoseInputHandlers() { + document.querySelectorAll('.glucose-input').forEach(input => { + // Avoid double-binding + if (input.dataset.glucoseBound) return; + input.dataset.glucoseBound = 'true'; - // Attach save-on-blur handlers - grid.querySelectorAll('.glucose-input').forEach(input => { input.addEventListener('blur', () => { const moment = input.dataset.moment; const val = input.value.trim() === '' ? null : parseFloat(input.value); @@ -2778,11 +2844,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- if (glucose[activeDate][moment] !== val) { glucose[activeDate][moment] = val; saveGlucoseToStorage(); - renderGlucoseSection(); + updateGlucoseUI(moment); } }); - // Enter key also saves input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { input.blur(); @@ -2791,6 +2856,28 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }); } + function updateGlucoseUI(momentKey) { + const m = PRICKMOMENTEN.find(p => p.key === momentKey); + if (!m) return; + + if (m.momentId === null) { + // Update nuchter card + const card = document.querySelector('.nuchter-card'); + if (card) { + card.outerHTML = renderNuchterCard(); + } + } else { + // Update inline glucose in eetmoment card + const el = document.querySelector(`.glucose-inline[data-moment-key="${momentKey}"]`); + if (el) { + el.outerHTML = renderInlineGlucoseItem(m); + } + } + + // Re-attach handlers on replaced elements + attachGlucoseInputHandlers(); + } + // ===== Daily Carb Limit ===== const DEFAULT_CARB_LIMIT = 175; // grams per day @@ -3184,12 +3271,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Progress bar renderProgressBar(totaal); - // Glucose section - renderGlucoseSection(); - - // Eating moments + // Eating moments — prepend nuchter card, then eetmoment cards with inline glucose const listEl = document.getElementById('eetmomentList'); - listEl.innerHTML = EETMOMENTEN.map((moment, mi) => { + const nuchterHtml = renderNuchterCard(); + listEl.innerHTML = nuchterHtml + EETMOMENTEN.map((moment, mi) => { const items = dayData[moment.id] || []; const momentKh = berekenEetmomentKh(activeDate, moment.id); const roundedKh = Math.round(momentKh); @@ -3275,6 +3360,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }).join(''); const cardOverLimit = overLimit && momentKh > 0; + // Check if this eetmoment has a matching glucose moment + const glucoseMoment = PRICKMOMENTEN.find(p => p.momentId === moment.id); + const inlineGlucoseHtml = glucoseMoment ? renderInlineGlucoseItem(glucoseMoment) : ''; return `
  • @@ -3284,9 +3372,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${moment.name}
  • ${roundedKh > 0 ? formatKh(roundedKh) + ' g' : '—'} - ${items.length > 0 ? '' : ''} + ${items.length > 0 ? '' : ''} ${items.length > 0 ? `` : ''} + ${inlineGlucoseHtml}
    + Voedingsmiddel toevoegen
    @@ -3379,6 +3468,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Empty state const hasItems = EETMOMENTEN.some(m => (dayData[m.id] || []).length > 0); document.getElementById('diaryEmpty').classList.toggle('hidden', hasItems); + + // Attach glucose input handlers (nuchter card + inline glucose) + attachGlucoseInputHandlers(); } function showDetail(item) { diff --git a/index.html b/index.html index 33b64d6..617473d 100644 --- a/index.html +++ b/index.html @@ -465,96 +465,111 @@ body { background: var(--progress-red); } -/* ===== Glucose Section ===== */ -.glucose-section { +/* ===== Nuchter card — compact glucose card vóór Ontbijt ===== */ +.nuchter-card { background: var(--white); border-radius: var(--radius); box-shadow: var(--shadow-sm); - padding: 14px 16px; + overflow: hidden; margin-bottom: 10px; -} - -.glucose-header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 12px; -} - -.glucose-icon { - font-size: 1.1rem; - color: var(--progress-red); -} - -.glucose-title { - font-weight: 700; - font-size: var(--font-md); - color: var(--text); -} - -.glucose-unit-label { - font-size: var(--font-xs); - color: var(--text-muted); - margin-left: 2px; -} - -/* Grid: 4 columns on wide, 2×2 on narrow */ -.glucose-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; -} - -@media (max-width: 480px) { - .glucose-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -.glucose-item { - background: var(--bg); - border-radius: var(--radius-sm); - padding: 10px 8px; - text-align: center; border: 1px solid var(--border); transition: border-color 0.2s, background 0.2s; + list-style: none; } - -.glucose-item.glucose-good { +.nuchter-card.glucose-good { border-color: #28a745; background: #f0fdf4; } - -.glucose-item.glucose-bad { +.nuchter-card.glucose-bad { border-color: var(--progress-red); background: #fef2f2; } - -.glucose-item.glucose-empty { +.nuchter-card.glucose-empty { border-color: var(--border); - background: var(--bg); + background: var(--white); +} +.nuchter-card-header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: var(--primary-lighter); +} +.nuchter-card-icon { + font-size: 1.1rem; +} +.nuchter-card-label { + font-weight: 600; + font-size: var(--font-sm); + color: var(--text); +} +.nuchter-card-range { + font-size: var(--font-xs); + color: var(--text-muted); + margin-left: auto; +} +.nuchter-card-body { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + gap: 12px; +} +.nuchter-card-body .glucose-input-wrapper { + margin-bottom: 0; } -.glucose-item-label { +/* ===== Inline glucose in eetmoment card ===== */ +.glucose-inline { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 14px; + border-top: 1px solid #F0F0F0; + gap: 8px; + transition: background 0.15s; +} +.glucose-inline.glucose-good { + background: #f0fdf4; +} +.glucose-inline.glucose-bad { + background: #fef2f2; +} +.glucose-inline.glucose-empty { + background: transparent; +} +.glucose-inline-left { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} +.glucose-inline-icon { + font-size: 0.9rem; + color: var(--progress-red); +} +.glucose-inline-label { font-size: var(--font-xs); font-weight: 600; color: var(--text-secondary); - margin-bottom: 2px; - line-height: 1.3; } - -.glucose-item-range { +.glucose-inline-range { font-size: 0.65rem; color: var(--text-muted); - margin-bottom: 8px; } +.glucose-inline-right { + display: flex; + align-items: center; + gap: 8px; +} + +/* ===== Shared glucose input styles ===== */ .glucose-input-wrapper { display: flex; align-items: center; justify-content: center; gap: 2px; - margin-bottom: 6px; } .glucose-input { @@ -2258,15 +2273,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
    - -
    -
    - - Glucose - mmol/L -
    -
    -
    + @@ -2455,10 +2462,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // ===== Prikmomenten voor glucose ===== const PRICKMOMENTEN = [ - { key: 'nuchter', label: 'Nuchter (voor ontbijt)', targetMin: 0, targetMax: 5.4 }, - { key: 'naOntbijt', label: '2 uur na ontbijt', targetMin: 5.4, targetMax: 6.7 }, - { key: 'naLunch', label: '2 uur na lunch', targetMin: 5.4, targetMax: 6.7 }, - { key: 'naAvondeten', label: '2 uur na avondeten', targetMin: 5.4, targetMax: 6.7 } + { key: 'nuchter', label: 'Nuchter (voor ontbijt)', targetMin: 0, targetMax: 5.4, momentId: null }, + { key: 'naOntbijt', label: '2 uur na ontbijt', targetMin: 5.4, targetMax: 6.7, momentId: 'Ontbijt' }, + { key: 'naLunch', label: '2 uur na lunch', targetMin: 5.4, targetMax: 6.7, momentId: 'Lunch' }, + { key: 'naAvondeten', label: '2 uur na avondeten', targetMin: 5.4, targetMax: 6.7, momentId: 'Avondeten' } ]; // ===== Standaard portiegroottes per categorie ===== @@ -2704,48 +2711,102 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } } - function renderGlucoseSection() { - const grid = document.getElementById('glucoseGrid'); - if (!grid) return; + // ===== Glucose — Inline per eetmoment ===== + function renderNuchterCard() { + const m = PRICKMOMENTEN.find(p => p.key === 'nuchter'); + if (!m) return ''; ensureGlucoseDay(activeDate); + const value = glucose[activeDate]?.['nuchter'] ?? null; + const status = isGlucoseInRange(value, 'nuchter'); - grid.innerHTML = PRICKMOMENTEN.map(m => { - const value = glucose[activeDate][m.key]; - const status = isGlucoseInRange(value, m.key); + let statusIcon, statusClass, statusText; + if (value === null || value === undefined || value === '') { + statusIcon = '⏹️'; + statusClass = 'glucose-empty'; + statusText = 'Nog meten'; + } else if (status === 'good') { + statusIcon = '✅'; + statusClass = 'glucose-good'; + statusText = 'Binnen range'; + } else if (status === 'high') { + statusIcon = '🔴'; + statusClass = 'glucose-bad'; + statusText = 'Te hoog'; + } else { + statusIcon = '🔴'; + statusClass = 'glucose-bad'; + statusText = 'Te laag'; + } - let statusIcon, statusClass, statusText; - if (value === null || value === undefined || value === '') { - statusIcon = '⏹️'; - statusClass = 'glucose-empty'; - statusText = 'Nog meten'; - } else if (status === 'good') { - statusIcon = '✅'; - statusClass = 'glucose-good'; - statusText = 'Binnen range'; - } else if (status === 'high') { - statusIcon = '🔴'; - statusClass = 'glucose-bad'; - statusText = 'Te hoog'; - } else { - statusIcon = '🔴'; - statusClass = 'glucose-bad'; - statusText = 'Te laag'; - } + const displayValue = (value !== null && value !== undefined) ? value : ''; - const displayValue = (value !== null && value !== undefined) ? value : ''; + return ` +
  • +
    + 🩸 + Nuchter (voor ontbijt) + < ${m.targetMax} +
    +
    +
    + + mmol/L +
    +
    + ${statusIcon} + ${statusText} +
    +
    +
  • + `; + } - // Short label: remove parenthetical and "2 uur na" prefix - let shortLabel = m.label; - shortLabel = shortLabel.replace(/\([^)]*\)/g, '').trim(); - shortLabel = shortLabel.replace(/^2 uur na /i, '').trim(); - // Capitalize first letter - shortLabel = shortLabel.charAt(0).toUpperCase() + shortLabel.slice(1); + function renderInlineGlucoseItem(m) { + ensureGlucoseDay(activeDate); + const value = glucose[activeDate]?.[m.key] ?? null; + const status = isGlucoseInRange(value, m.key); - return ` -
    -
    ${shortLabel}
    -
    ${m.key === 'nuchter' ? '< ' + m.targetMax : m.targetMin + '–' + m.targetMax}
    + let statusIcon, statusClass, statusText; + if (value === null || value === undefined || value === '') { + statusIcon = '⏹️'; + statusClass = 'glucose-empty'; + statusText = 'Nog meten'; + } else if (status === 'good') { + statusIcon = '✅'; + statusClass = 'glucose-good'; + statusText = 'Binnen range'; + } else if (status === 'high') { + statusIcon = '🔴'; + statusClass = 'glucose-bad'; + statusText = 'Te hoog'; + } else { + statusIcon = '🔴'; + statusClass = 'glucose-bad'; + statusText = 'Te laag'; + } + + const displayValue = (value !== null && value !== undefined) ? value : ''; + + // Short label: remove parenthetical and "2 uur na" prefix + let shortLabel = m.label; + shortLabel = shortLabel.replace(/\([^)]*\)/g, '').trim(); + shortLabel = shortLabel.replace(/^2 uur na /i, '').trim(); + shortLabel = shortLabel.charAt(0).toUpperCase() + shortLabel.slice(1); + + return ` +
    +
    + 🩸 + ${shortLabel} + ${m.targetMin}–${m.targetMax} +
    +
    ${statusText}
    - `; - }).join(''); +
    + `; + } + + function attachGlucoseInputHandlers() { + document.querySelectorAll('.glucose-input').forEach(input => { + // Avoid double-binding + if (input.dataset.glucoseBound) return; + input.dataset.glucoseBound = 'true'; - // Attach save-on-blur handlers - grid.querySelectorAll('.glucose-input').forEach(input => { input.addEventListener('blur', () => { const moment = input.dataset.moment; const val = input.value.trim() === '' ? null : parseFloat(input.value); @@ -2778,11 +2844,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- if (glucose[activeDate][moment] !== val) { glucose[activeDate][moment] = val; saveGlucoseToStorage(); - renderGlucoseSection(); + updateGlucoseUI(moment); } }); - // Enter key also saves input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { input.blur(); @@ -2791,6 +2856,28 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }); } + function updateGlucoseUI(momentKey) { + const m = PRICKMOMENTEN.find(p => p.key === momentKey); + if (!m) return; + + if (m.momentId === null) { + // Update nuchter card + const card = document.querySelector('.nuchter-card'); + if (card) { + card.outerHTML = renderNuchterCard(); + } + } else { + // Update inline glucose in eetmoment card + const el = document.querySelector(`.glucose-inline[data-moment-key="${momentKey}"]`); + if (el) { + el.outerHTML = renderInlineGlucoseItem(m); + } + } + + // Re-attach handlers on replaced elements + attachGlucoseInputHandlers(); + } + // ===== Daily Carb Limit ===== const DEFAULT_CARB_LIMIT = 175; // grams per day @@ -3184,12 +3271,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Progress bar renderProgressBar(totaal); - // Glucose section - renderGlucoseSection(); - - // Eating moments + // Eating moments — prepend nuchter card, then eetmoment cards with inline glucose const listEl = document.getElementById('eetmomentList'); - listEl.innerHTML = EETMOMENTEN.map((moment, mi) => { + const nuchterHtml = renderNuchterCard(); + listEl.innerHTML = nuchterHtml + EETMOMENTEN.map((moment, mi) => { const items = dayData[moment.id] || []; const momentKh = berekenEetmomentKh(activeDate, moment.id); const roundedKh = Math.round(momentKh); @@ -3275,6 +3360,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- }).join(''); const cardOverLimit = overLimit && momentKh > 0; + // Check if this eetmoment has a matching glucose moment + const glucoseMoment = PRICKMOMENTEN.find(p => p.momentId === moment.id); + const inlineGlucoseHtml = glucoseMoment ? renderInlineGlucoseItem(glucoseMoment) : ''; return `
  • @@ -3284,9 +3372,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- ${moment.name}
  • ${roundedKh > 0 ? formatKh(roundedKh) + ' g' : '—'} - ${items.length > 0 ? '' : ''} + ${items.length > 0 ? '' : ''} ${items.length > 0 ? `` : ''} + ${inlineGlucoseHtml}
    + Voedingsmiddel toevoegen
    @@ -3379,6 +3468,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Empty state const hasItems = EETMOMENTEN.some(m => (dayData[m.id] || []).length > 0); document.getElementById('diaryEmpty').classList.toggle('hidden', hasItems); + + // Attach glucose input handlers (nuchter card + inline glucose) + attachGlucoseInputHandlers(); } function showDetail(item) { diff --git a/tests/glucose-e2e.spec.js b/tests/glucose-e2e.spec.js index b8b02d9..07ff498 100644 --- a/tests/glucose-e2e.spec.js +++ b/tests/glucose-e2e.spec.js @@ -3,38 +3,41 @@ const { test, expect } = require('@playwright/test'); const BASE = 'https://eetdagboek.vantwout.dev'; /** - * Helpers for glucose tests + * Helpers for glucose tests — inline per eetmoment */ /** Navigate to diary tab and wait for render */ async function openDiary(page) { await page.locator('#navDagboek').click(); await page.waitForTimeout(600); - // Wait for glucose section to render - await expect(page.locator('.glucose-section')).toBeVisible({ timeout: 5000 }); + // Wait for nuchter card to render + await expect(page.locator('.nuchter-card')).toBeVisible({ timeout: 5000 }); } /** Fill a glucose input and trigger save-on-blur */ async function setGlucoseValue(page, momentKey, value) { const input = page.locator(`input.glucose-input[data-moment="${momentKey}"]`); await input.fill(String(value)); - // Trigger blur — saves to localStorage and re-renders the grid + // Trigger blur — saves to localStorage and re-renders just the glucose UI await input.evaluate(el => el.blur()); // Wait for re-render to complete await page.waitForTimeout(400); } -/** Get the parent .glucose-item for a given moment */ -function glucoseItem(page, momentKey) { - return page.locator(`input.glucose-input[data-moment="${momentKey}"]`).locator('..').locator('..'); +/** Get the parent glucose container for a given moment */ +function glucoseContainer(page, momentKey) { + if (momentKey === 'nuchter') { + return page.locator('.nuchter-card'); + } + return page.locator(`.glucose-inline[data-moment-key="${momentKey}"]`); } /** Get status text element for a given moment */ function statusText(page, momentKey) { - return page.locator(`input.glucose-input[data-moment="${momentKey}"]`) - .locator('..') // .glucose-input-wrapper - .locator('..') // .glucose-item - .locator('.glucose-status-text'); + if (momentKey === 'nuchter') { + return page.locator('.nuchter-card .glucose-status-text'); + } + return page.locator(`.glucose-inline[data-moment-key="${momentKey}"] .glucose-status-text`); } // ============================================================================= @@ -47,33 +50,33 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent await page.goto(BASE); await openDiary(page); - // Section structure - await expect(page.locator('.glucose-section')).toBeVisible(); - await expect(page.locator('.glucose-title')).toHaveText('Glucose'); - await expect(page.locator('.glucose-unit-label')).toHaveText('mmol/L'); - await expect(page.locator('.glucose-header .glucose-icon i')).toBeVisible(); + // Nuchter card structure + await expect(page.locator('.nuchter-card')).toBeVisible(); + await expect(page.locator('.nuchter-card-label')).toHaveText('Nuchter (voor ontbijt)'); + await expect(page.locator('.nuchter-card-range')).toHaveText('< 5.4'); - // 4 grid items - const items = page.locator('.glucose-item'); - await expect(items).toHaveCount(4); + // Inline glucose in eetmoment cards (Ontbijt, Lunch, Avondeten) + const moments = [ + { key: 'naOntbijt', label: 'Ontbijt', range: '5.4\u20136.7' }, + { key: 'naLunch', label: 'Lunch', range: '5.4\u20136.7' }, + { key: 'naAvondeten', label: 'Avondeten', range: '5.4\u20136.7' }, + ]; - // Labels - await expect(items.nth(0).locator('.glucose-item-label')).toHaveText('Nuchter'); - await expect(items.nth(1).locator('.glucose-item-label')).toHaveText('Ontbijt'); - await expect(items.nth(2).locator('.glucose-item-label')).toHaveText('Lunch'); - await expect(items.nth(3).locator('.glucose-item-label')).toHaveText('Avondeten'); + // 3 inline glucose items should exist + const inlineItems = page.locator('.glucose-inline'); + await expect(inlineItems).toHaveCount(3); - // Target ranges - await expect(items.nth(0).locator('.glucose-item-range')).toHaveText('< 5.4'); - await expect(items.nth(1).locator('.glucose-item-range')).toHaveText('5.4\u20136.7'); - await expect(items.nth(2).locator('.glucose-item-range')).toHaveText('5.4\u20136.7'); - await expect(items.nth(3).locator('.glucose-item-range')).toHaveText('5.4\u20136.7'); + for (let i = 0; i < moments.length; i++) { + const m = moments[i]; + const container = page.locator(`.glucose-inline[data-moment-key="${m.key}"]`); + await expect(container.locator('.glucose-inline-label')).toHaveText(m.label); + await expect(container.locator('.glucose-inline-range')).toHaveText(m.range); + } - // Input attributes - const moments = ['nuchter', 'naOntbijt', 'naLunch', 'naAvondeten']; - for (let i = 0; i < 4; i++) { - const input = items.nth(i).locator('.glucose-input'); - await expect(input).toHaveAttribute('data-moment', moments[i]); + // Input attributes — check 4 inputs + const inputMoments = ['nuchter', 'naOntbijt', 'naLunch', 'naAvondeten']; + for (const moment of inputMoments) { + const input = page.locator(`input.glucose-input[data-moment="${moment}"]`); await expect(input).toHaveAttribute('step', '0.1'); await expect(input).toHaveAttribute('min', '0'); await expect(input).toHaveAttribute('max', '30'); @@ -90,11 +93,10 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent await setGlucoseValue(page, 'nuchter', 5.1); - // Check the item class and status - const item = glucoseItem(page, 'nuchter'); - await expect(item).toHaveClass(/glucose-good/); - await expect(item.locator('.glucose-status-icon')).toContainText('\u2705'); // ✅ - await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range'); + const container = glucoseContainer(page, 'nuchter'); + await expect(container).toHaveClass(/glucose-good/); + await expect(container.locator('.glucose-status-icon')).toContainText('\u2705'); // ✅ + await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range'); }); // ---- 3. Nuchter ≥ 5.4 (rood) ---- @@ -105,9 +107,9 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent await setGlucoseValue(page, 'nuchter', 5.4); - const item = glucoseItem(page, 'nuchter'); - await expect(item).toHaveClass(/glucose-bad/); - await expect(item.locator('.glucose-status-text')).toHaveText('Te hoog'); + const container = glucoseContainer(page, 'nuchter'); + await expect(container).toHaveClass(/glucose-bad/); + await expect(container.locator('.glucose-status-text')).toHaveText('Te hoog'); }); // ---- 4. Postprandiaal < 5.4 (rood/te laag) ---- @@ -118,9 +120,9 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent await setGlucoseValue(page, 'naOntbijt', 5.3); - const item = glucoseItem(page, 'naOntbijt'); - await expect(item).toHaveClass(/glucose-bad/); - await expect(item.locator('.glucose-status-text')).toHaveText('Te laag'); + const container = glucoseContainer(page, 'naOntbijt'); + await expect(container).toHaveClass(/glucose-bad/); + await expect(container.locator('.glucose-status-text')).toHaveText('Te laag'); }); // ---- 5. Postprandiaal binnen range (groen) ---- @@ -131,9 +133,9 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent await setGlucoseValue(page, 'naOntbijt', 6.0); - const item = glucoseItem(page, 'naOntbijt'); - await expect(item).toHaveClass(/glucose-good/); - await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range'); + const container = glucoseContainer(page, 'naOntbijt'); + await expect(container).toHaveClass(/glucose-good/); + await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range'); }); // ---- 6. Postprandiaal > 6.7 (rood/te hoog) ---- @@ -144,9 +146,9 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent await setGlucoseValue(page, 'naLunch', 6.8); - const item = glucoseItem(page, 'naLunch'); - await expect(item).toHaveClass(/glucose-bad/); - await expect(item.locator('.glucose-status-text')).toHaveText('Te hoog'); + const container = glucoseContainer(page, 'naLunch'); + await expect(container).toHaveClass(/glucose-bad/); + await expect(container.locator('.glucose-status-text')).toHaveText('Te hoog'); }); // ---- 7. Leeg veld — grijze 'nog meten' indicator ---- @@ -155,31 +157,24 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent await page.goto(BASE); await openDiary(page); - // All 4 items should start empty - const items = page.locator('.glucose-item'); - const count = await items.count(); - expect(count).toBe(4); + // Nuchter card should start empty + const nuchter = glucoseContainer(page, 'nuchter'); + await expect(nuchter).toHaveClass(/glucose-empty/); + await expect(nuchter.locator('.glucose-status-text')).toHaveText('Nog meten'); + await expect(nuchter.locator('.glucose-status-icon')).toContainText('\u23F9'); - for (let i = 0; i < count; i++) { - await expect(items.nth(i)).toHaveClass(/glucose-empty/); - } - - const statusTexts = page.locator('.glucose-status-text'); - const texts = await statusTexts.allTextContents(); - for (const t of texts) { - expect(t).toBe('Nog meten'); - } - - // Status icons should be ⏹️ - const icons = page.locator('.glucose-status-icon'); - const iconTexts = await icons.allTextContents(); - for (const icon of iconTexts) { - expect(icon.includes('\u23F9\uFE0F') || icon.includes('\u23F9')).toBeTruthy(); + // Inline items should start empty + const inlineMoments = ['naOntbijt', 'naLunch', 'naAvondeten']; + for (const moment of inlineMoments) { + const container = glucoseContainer(page, moment); + await expect(container).toHaveClass(/glucose-empty/); + await expect(container.locator('.glucose-status-text')).toHaveText('Nog meten'); } // All inputs should have empty value const inputs = page.locator('.glucose-input'); const inputCount = await inputs.count(); + expect(inputCount).toBe(4); for (let i = 0; i < inputCount; i++) { await expect(inputs.nth(i)).toHaveValue(''); } @@ -195,20 +190,20 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent await setGlucoseValue(page, 'nuchter', 5.1); // Verify it's shown correctly - let item = glucoseItem(page, 'nuchter'); - await expect(item).toHaveClass(/glucose-good/); - await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range'); - await expect(item.locator('.glucose-input')).toHaveValue('5.1'); + let container = glucoseContainer(page, 'nuchter'); + await expect(container).toHaveClass(/glucose-good/); + await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range'); + await expect(container.locator('.glucose-input')).toHaveValue('5.1'); // Reload the page await page.goto(BASE); await openDiary(page); // Value should still be there - item = glucoseItem(page, 'nuchter'); - await expect(item).toHaveClass(/glucose-good/); - await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range'); - await expect(item.locator('.glucose-input')).toHaveValue('5.1'); + container = glucoseContainer(page, 'nuchter'); + await expect(container).toHaveClass(/glucose-good/); + await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range'); + await expect(container.locator('.glucose-input')).toHaveValue('5.1'); }); // ---- 9. Datum-navigatie — andere datum andere waarden ---- @@ -221,23 +216,23 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent await setGlucoseValue(page, 'nuchter', 5.1); // Verify today shows the value - let item = glucoseItem(page, 'nuchter'); - await expect(item).toHaveClass(/glucose-good/); + let container = glucoseContainer(page, 'nuchter'); + await expect(container).toHaveClass(/glucose-good/); // Navigate to tomorrow await page.locator('#dayNext').click(); await page.waitForTimeout(600); // Tomorrow should have all empty glucose fields - item = glucoseItem(page, 'nuchter'); - await expect(item).toHaveClass(/glucose-empty/); - await expect(item.locator('.glucose-status-text')).toHaveText('Nog meten'); - await expect(item.locator('.glucose-input')).toHaveValue(''); + container = glucoseContainer(page, 'nuchter'); + await expect(container).toHaveClass(/glucose-empty/); + await expect(container.locator('.glucose-status-text')).toHaveText('Nog meten'); + await expect(container.locator('.glucose-input')).toHaveValue(''); // Also check the other moments are empty too for (const moment of ['naOntbijt', 'naLunch', 'naAvondeten']) { - const otherItem = glucoseItem(page, moment); - await expect(otherItem).toHaveClass(/glucose-empty/); + const otherContainer = glucoseContainer(page, moment); + await expect(otherContainer).toHaveClass(/glucose-empty/); } // Navigate back to today @@ -245,10 +240,10 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent await page.waitForTimeout(600); // Today's value should still be there - item = glucoseItem(page, 'nuchter'); - await expect(item).toHaveClass(/glucose-good/); - await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range'); - await expect(item.locator('.glucose-input')).toHaveValue('5.1'); + container = glucoseContainer(page, 'nuchter'); + await expect(container).toHaveClass(/glucose-good/); + await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range'); + await expect(container.locator('.glucose-input')).toHaveValue('5.1'); }); // ---- 10. Decimale invoer — step 0.1 ---- @@ -263,18 +258,18 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent // Enter 5.5 (decimal) await setGlucoseValue(page, 'nuchter', 5.5); - let item = glucoseItem(page, 'nuchter'); - await expect(item.locator('.glucose-input')).toHaveValue('5.5'); + let container = glucoseContainer(page, 'nuchter'); + await expect(container.locator('.glucose-input')).toHaveValue('5.5'); // 5.5 ≥ 5.4 for nuchter → should be bad/Te hoog - await expect(item).toHaveClass(/glucose-bad/); - await expect(item.locator('.glucose-status-text')).toHaveText('Te hoog'); + await expect(container).toHaveClass(/glucose-bad/); + await expect(container.locator('.glucose-status-text')).toHaveText('Te hoog'); // Now test a postprandial decimal value await setGlucoseValue(page, 'naOntbijt', 6.2); - item = glucoseItem(page, 'naOntbijt'); - await expect(item.locator('.glucose-input')).toHaveValue('6.2'); - await expect(item).toHaveClass(/glucose-good/); - await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range'); + container = glucoseContainer(page, 'naOntbijt'); + await expect(container.locator('.glucose-input')).toHaveValue('6.2'); + await expect(container).toHaveClass(/glucose-good/); + await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range'); }); });