UX: Glucose inline per eetmoment — nuchter card + inline postprandiale metingen
This commit is contained in:
parent
cfcb77407f
commit
2309cf22ad
3 changed files with 519 additions and 340 deletions
|
|
@ -465,96 +465,111 @@ body {
|
||||||
background: var(--progress-red);
|
background: var(--progress-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Glucose Section ===== */
|
/* ===== Nuchter card — compact glucose card vóór Ontbijt ===== */
|
||||||
.glucose-section {
|
.nuchter-card {
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
padding: 14px 16px;
|
overflow: hidden;
|
||||||
margin-bottom: 10px;
|
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);
|
border: 1px solid var(--border);
|
||||||
transition: border-color 0.2s, background 0.2s;
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
.nuchter-card.glucose-good {
|
||||||
.glucose-item.glucose-good {
|
|
||||||
border-color: #28a745;
|
border-color: #28a745;
|
||||||
background: #f0fdf4;
|
background: #f0fdf4;
|
||||||
}
|
}
|
||||||
|
.nuchter-card.glucose-bad {
|
||||||
.glucose-item.glucose-bad {
|
|
||||||
border-color: var(--progress-red);
|
border-color: var(--progress-red);
|
||||||
background: #fef2f2;
|
background: #fef2f2;
|
||||||
}
|
}
|
||||||
|
.nuchter-card.glucose-empty {
|
||||||
.glucose-item.glucose-empty {
|
|
||||||
border-color: var(--border);
|
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-size: var(--font-xs);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-bottom: 2px;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
}
|
||||||
|
.glucose-inline-range {
|
||||||
.glucose-item-range {
|
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
.glucose-inline-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Shared glucose input styles ===== */
|
||||||
|
|
||||||
.glucose-input-wrapper {
|
.glucose-input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.glucose-input {
|
.glucose-input {
|
||||||
|
|
@ -2258,15 +2273,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<div class="carb-warning" id="carbWarning"></div>
|
<div class="carb-warning" id="carbWarning"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Glucose Section -->
|
<!-- Glucose Section removed — inline in eetmoment cards -->
|
||||||
<div class="glucose-section" id="glucoseSection">
|
|
||||||
<div class="glucose-header">
|
|
||||||
<span class="glucose-icon"><i class="fa-solid fa-droplet"></i></span>
|
|
||||||
<span class="glucose-title">Glucose</span>
|
|
||||||
<span class="glucose-unit-label">mmol/L</span>
|
|
||||||
</div>
|
|
||||||
<div class="glucose-grid" id="glucoseGrid"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="eetmoment-list" id="eetmomentList"></ul>
|
<ul class="eetmoment-list" id="eetmomentList"></ul>
|
||||||
|
|
||||||
|
|
@ -2455,10 +2462,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
|
|
||||||
// ===== Prikmomenten voor glucose =====
|
// ===== Prikmomenten voor glucose =====
|
||||||
const PRICKMOMENTEN = [
|
const PRICKMOMENTEN = [
|
||||||
{ key: 'nuchter', label: 'Nuchter (voor ontbijt)', targetMin: 0, targetMax: 5.4 },
|
{ 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 },
|
{ 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 },
|
{ 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 }
|
{ key: 'naAvondeten', label: '2 uur na avondeten', targetMin: 5.4, targetMax: 6.7, momentId: 'Avondeten' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// ===== Standaard portiegroottes per categorie =====
|
// ===== Standaard portiegroottes per categorie =====
|
||||||
|
|
@ -2704,14 +2711,65 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGlucoseSection() {
|
// ===== Glucose — Inline per eetmoment =====
|
||||||
const grid = document.getElementById('glucoseGrid');
|
|
||||||
if (!grid) return;
|
|
||||||
|
|
||||||
|
function renderNuchterCard() {
|
||||||
|
const m = PRICKMOMENTEN.find(p => p.key === 'nuchter');
|
||||||
|
if (!m) return '';
|
||||||
ensureGlucoseDay(activeDate);
|
ensureGlucoseDay(activeDate);
|
||||||
|
const value = glucose[activeDate]?.['nuchter'] ?? null;
|
||||||
|
const status = isGlucoseInRange(value, 'nuchter');
|
||||||
|
|
||||||
grid.innerHTML = PRICKMOMENTEN.map(m => {
|
let statusIcon, statusClass, statusText;
|
||||||
const value = glucose[activeDate][m.key];
|
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 : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li class="nuchter-card ${statusClass}">
|
||||||
|
<div class="nuchter-card-header">
|
||||||
|
<span class="nuchter-card-icon">🩸</span>
|
||||||
|
<span class="nuchter-card-label">Nuchter (voor ontbijt)</span>
|
||||||
|
<span class="nuchter-card-range">< ${m.targetMax}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nuchter-card-body">
|
||||||
|
<div class="glucose-input-wrapper">
|
||||||
|
<input type="number" class="glucose-input"
|
||||||
|
value="${displayValue}"
|
||||||
|
data-moment="nuchter"
|
||||||
|
step="0.1" min="0" max="30"
|
||||||
|
placeholder="—"
|
||||||
|
inputmode="decimal">
|
||||||
|
<span class="glucose-unit">mmol/L</span>
|
||||||
|
</div>
|
||||||
|
<div class="glucose-status ${statusClass}">
|
||||||
|
<span class="glucose-status-icon">${statusIcon}</span>
|
||||||
|
<span class="glucose-status-text">${statusText}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInlineGlucoseItem(m) {
|
||||||
|
ensureGlucoseDay(activeDate);
|
||||||
|
const value = glucose[activeDate]?.[m.key] ?? null;
|
||||||
const status = isGlucoseInRange(value, m.key);
|
const status = isGlucoseInRange(value, m.key);
|
||||||
|
|
||||||
let statusIcon, statusClass, statusText;
|
let statusIcon, statusClass, statusText;
|
||||||
|
|
@ -2739,13 +2797,16 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
let shortLabel = m.label;
|
let shortLabel = m.label;
|
||||||
shortLabel = shortLabel.replace(/\([^)]*\)/g, '').trim();
|
shortLabel = shortLabel.replace(/\([^)]*\)/g, '').trim();
|
||||||
shortLabel = shortLabel.replace(/^2 uur na /i, '').trim();
|
shortLabel = shortLabel.replace(/^2 uur na /i, '').trim();
|
||||||
// Capitalize first letter
|
|
||||||
shortLabel = shortLabel.charAt(0).toUpperCase() + shortLabel.slice(1);
|
shortLabel = shortLabel.charAt(0).toUpperCase() + shortLabel.slice(1);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="glucose-item ${statusClass}">
|
<div class="glucose-inline ${statusClass}" data-moment-key="${m.key}">
|
||||||
<div class="glucose-item-label">${shortLabel}</div>
|
<div class="glucose-inline-left">
|
||||||
<div class="glucose-item-range">${m.key === 'nuchter' ? '< ' + m.targetMax : m.targetMin + '–' + m.targetMax}</div>
|
<span class="glucose-inline-icon">🩸</span>
|
||||||
|
<span class="glucose-inline-label">${shortLabel}</span>
|
||||||
|
<span class="glucose-inline-range">${m.targetMin}–${m.targetMax}</span>
|
||||||
|
</div>
|
||||||
|
<div class="glucose-inline-right">
|
||||||
<div class="glucose-input-wrapper">
|
<div class="glucose-input-wrapper">
|
||||||
<input type="number" class="glucose-input"
|
<input type="number" class="glucose-input"
|
||||||
value="${displayValue}"
|
value="${displayValue}"
|
||||||
|
|
@ -2760,11 +2821,16 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<span class="glucose-status-text">${statusText}</span>
|
<span class="glucose-status-text">${statusText}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}).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', () => {
|
input.addEventListener('blur', () => {
|
||||||
const moment = input.dataset.moment;
|
const moment = input.dataset.moment;
|
||||||
const val = input.value.trim() === '' ? null : parseFloat(input.value);
|
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) {
|
if (glucose[activeDate][moment] !== val) {
|
||||||
glucose[activeDate][moment] = val;
|
glucose[activeDate][moment] = val;
|
||||||
saveGlucoseToStorage();
|
saveGlucoseToStorage();
|
||||||
renderGlucoseSection();
|
updateGlucoseUI(moment);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enter key also saves
|
|
||||||
input.addEventListener('keydown', (e) => {
|
input.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
input.blur();
|
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 =====
|
// ===== Daily Carb Limit =====
|
||||||
const DEFAULT_CARB_LIMIT = 175; // grams per day
|
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
|
// Progress bar
|
||||||
renderProgressBar(totaal);
|
renderProgressBar(totaal);
|
||||||
|
|
||||||
// Glucose section
|
// Eating moments — prepend nuchter card, then eetmoment cards with inline glucose
|
||||||
renderGlucoseSection();
|
|
||||||
|
|
||||||
// Eating moments
|
|
||||||
const listEl = document.getElementById('eetmomentList');
|
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 items = dayData[moment.id] || [];
|
||||||
const momentKh = berekenEetmomentKh(activeDate, moment.id);
|
const momentKh = berekenEetmomentKh(activeDate, moment.id);
|
||||||
const roundedKh = Math.round(momentKh);
|
const roundedKh = Math.round(momentKh);
|
||||||
|
|
@ -3275,6 +3360,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const cardOverLimit = overLimit && momentKh > 0;
|
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 `
|
return `
|
||||||
<li class="eetmoment-card${cardOverLimit ? ' over-limit' : ''}" style="--i:${mi}">
|
<li class="eetmoment-card${cardOverLimit ? ' over-limit' : ''}" style="--i:${mi}">
|
||||||
|
|
@ -3284,9 +3372,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<span class="eetmoment-name">${moment.name}</span>
|
<span class="eetmoment-name">${moment.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="eetmoment-kh">${roundedKh > 0 ? formatKh(roundedKh) + ' g' : '—'}</span>
|
<span class="eetmoment-kh">${roundedKh > 0 ? formatKh(roundedKh) + ' g' : '—'}</span>
|
||||||
<span style="font-size:0.8rem;color:var(--text-muted)">${items.length > 0 ? '<i class="fas fa-chevron-down"></i>' : ''}</span>
|
<span style="font-size:0.8rem;color:var(--text-muted)">${items.length > 0 ? '<i class=\"fas fa-chevron-down\"></i>' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
${items.length > 0 ? `<ul class="eetmoment-items">${itemsHtml}</ul>` : ''}
|
${items.length > 0 ? `<ul class="eetmoment-items">${itemsHtml}</ul>` : ''}
|
||||||
|
${inlineGlucoseHtml}
|
||||||
<div class="eetmoment-add-btn" data-moment="${moment.id}">
|
<div class="eetmoment-add-btn" data-moment="${moment.id}">
|
||||||
<span>+</span> Voedingsmiddel toevoegen
|
<span>+</span> Voedingsmiddel toevoegen
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3379,6 +3468,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
// Empty state
|
// Empty state
|
||||||
const hasItems = EETMOMENTEN.some(m => (dayData[m.id] || []).length > 0);
|
const hasItems = EETMOMENTEN.some(m => (dayData[m.id] || []).length > 0);
|
||||||
document.getElementById('diaryEmpty').classList.toggle('hidden', hasItems);
|
document.getElementById('diaryEmpty').classList.toggle('hidden', hasItems);
|
||||||
|
|
||||||
|
// Attach glucose input handlers (nuchter card + inline glucose)
|
||||||
|
attachGlucoseInputHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDetail(item) {
|
function showDetail(item) {
|
||||||
|
|
|
||||||
280
index.html
280
index.html
|
|
@ -465,96 +465,111 @@ body {
|
||||||
background: var(--progress-red);
|
background: var(--progress-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Glucose Section ===== */
|
/* ===== Nuchter card — compact glucose card vóór Ontbijt ===== */
|
||||||
.glucose-section {
|
.nuchter-card {
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
padding: 14px 16px;
|
overflow: hidden;
|
||||||
margin-bottom: 10px;
|
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);
|
border: 1px solid var(--border);
|
||||||
transition: border-color 0.2s, background 0.2s;
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
.nuchter-card.glucose-good {
|
||||||
.glucose-item.glucose-good {
|
|
||||||
border-color: #28a745;
|
border-color: #28a745;
|
||||||
background: #f0fdf4;
|
background: #f0fdf4;
|
||||||
}
|
}
|
||||||
|
.nuchter-card.glucose-bad {
|
||||||
.glucose-item.glucose-bad {
|
|
||||||
border-color: var(--progress-red);
|
border-color: var(--progress-red);
|
||||||
background: #fef2f2;
|
background: #fef2f2;
|
||||||
}
|
}
|
||||||
|
.nuchter-card.glucose-empty {
|
||||||
.glucose-item.glucose-empty {
|
|
||||||
border-color: var(--border);
|
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-size: var(--font-xs);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-bottom: 2px;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
}
|
||||||
|
.glucose-inline-range {
|
||||||
.glucose-item-range {
|
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
.glucose-inline-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Shared glucose input styles ===== */
|
||||||
|
|
||||||
.glucose-input-wrapper {
|
.glucose-input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.glucose-input {
|
.glucose-input {
|
||||||
|
|
@ -2258,15 +2273,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<div class="carb-warning" id="carbWarning"></div>
|
<div class="carb-warning" id="carbWarning"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Glucose Section -->
|
<!-- Glucose Section removed — inline in eetmoment cards -->
|
||||||
<div class="glucose-section" id="glucoseSection">
|
|
||||||
<div class="glucose-header">
|
|
||||||
<span class="glucose-icon"><i class="fa-solid fa-droplet"></i></span>
|
|
||||||
<span class="glucose-title">Glucose</span>
|
|
||||||
<span class="glucose-unit-label">mmol/L</span>
|
|
||||||
</div>
|
|
||||||
<div class="glucose-grid" id="glucoseGrid"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="eetmoment-list" id="eetmomentList"></ul>
|
<ul class="eetmoment-list" id="eetmomentList"></ul>
|
||||||
|
|
||||||
|
|
@ -2455,10 +2462,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
|
|
||||||
// ===== Prikmomenten voor glucose =====
|
// ===== Prikmomenten voor glucose =====
|
||||||
const PRICKMOMENTEN = [
|
const PRICKMOMENTEN = [
|
||||||
{ key: 'nuchter', label: 'Nuchter (voor ontbijt)', targetMin: 0, targetMax: 5.4 },
|
{ 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 },
|
{ 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 },
|
{ 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 }
|
{ key: 'naAvondeten', label: '2 uur na avondeten', targetMin: 5.4, targetMax: 6.7, momentId: 'Avondeten' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// ===== Standaard portiegroottes per categorie =====
|
// ===== Standaard portiegroottes per categorie =====
|
||||||
|
|
@ -2704,14 +2711,65 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGlucoseSection() {
|
// ===== Glucose — Inline per eetmoment =====
|
||||||
const grid = document.getElementById('glucoseGrid');
|
|
||||||
if (!grid) return;
|
|
||||||
|
|
||||||
|
function renderNuchterCard() {
|
||||||
|
const m = PRICKMOMENTEN.find(p => p.key === 'nuchter');
|
||||||
|
if (!m) return '';
|
||||||
ensureGlucoseDay(activeDate);
|
ensureGlucoseDay(activeDate);
|
||||||
|
const value = glucose[activeDate]?.['nuchter'] ?? null;
|
||||||
|
const status = isGlucoseInRange(value, 'nuchter');
|
||||||
|
|
||||||
grid.innerHTML = PRICKMOMENTEN.map(m => {
|
let statusIcon, statusClass, statusText;
|
||||||
const value = glucose[activeDate][m.key];
|
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 : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li class="nuchter-card ${statusClass}">
|
||||||
|
<div class="nuchter-card-header">
|
||||||
|
<span class="nuchter-card-icon">🩸</span>
|
||||||
|
<span class="nuchter-card-label">Nuchter (voor ontbijt)</span>
|
||||||
|
<span class="nuchter-card-range">< ${m.targetMax}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nuchter-card-body">
|
||||||
|
<div class="glucose-input-wrapper">
|
||||||
|
<input type="number" class="glucose-input"
|
||||||
|
value="${displayValue}"
|
||||||
|
data-moment="nuchter"
|
||||||
|
step="0.1" min="0" max="30"
|
||||||
|
placeholder="—"
|
||||||
|
inputmode="decimal">
|
||||||
|
<span class="glucose-unit">mmol/L</span>
|
||||||
|
</div>
|
||||||
|
<div class="glucose-status ${statusClass}">
|
||||||
|
<span class="glucose-status-icon">${statusIcon}</span>
|
||||||
|
<span class="glucose-status-text">${statusText}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInlineGlucoseItem(m) {
|
||||||
|
ensureGlucoseDay(activeDate);
|
||||||
|
const value = glucose[activeDate]?.[m.key] ?? null;
|
||||||
const status = isGlucoseInRange(value, m.key);
|
const status = isGlucoseInRange(value, m.key);
|
||||||
|
|
||||||
let statusIcon, statusClass, statusText;
|
let statusIcon, statusClass, statusText;
|
||||||
|
|
@ -2739,13 +2797,16 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
let shortLabel = m.label;
|
let shortLabel = m.label;
|
||||||
shortLabel = shortLabel.replace(/\([^)]*\)/g, '').trim();
|
shortLabel = shortLabel.replace(/\([^)]*\)/g, '').trim();
|
||||||
shortLabel = shortLabel.replace(/^2 uur na /i, '').trim();
|
shortLabel = shortLabel.replace(/^2 uur na /i, '').trim();
|
||||||
// Capitalize first letter
|
|
||||||
shortLabel = shortLabel.charAt(0).toUpperCase() + shortLabel.slice(1);
|
shortLabel = shortLabel.charAt(0).toUpperCase() + shortLabel.slice(1);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="glucose-item ${statusClass}">
|
<div class="glucose-inline ${statusClass}" data-moment-key="${m.key}">
|
||||||
<div class="glucose-item-label">${shortLabel}</div>
|
<div class="glucose-inline-left">
|
||||||
<div class="glucose-item-range">${m.key === 'nuchter' ? '< ' + m.targetMax : m.targetMin + '–' + m.targetMax}</div>
|
<span class="glucose-inline-icon">🩸</span>
|
||||||
|
<span class="glucose-inline-label">${shortLabel}</span>
|
||||||
|
<span class="glucose-inline-range">${m.targetMin}–${m.targetMax}</span>
|
||||||
|
</div>
|
||||||
|
<div class="glucose-inline-right">
|
||||||
<div class="glucose-input-wrapper">
|
<div class="glucose-input-wrapper">
|
||||||
<input type="number" class="glucose-input"
|
<input type="number" class="glucose-input"
|
||||||
value="${displayValue}"
|
value="${displayValue}"
|
||||||
|
|
@ -2760,11 +2821,16 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<span class="glucose-status-text">${statusText}</span>
|
<span class="glucose-status-text">${statusText}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}).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', () => {
|
input.addEventListener('blur', () => {
|
||||||
const moment = input.dataset.moment;
|
const moment = input.dataset.moment;
|
||||||
const val = input.value.trim() === '' ? null : parseFloat(input.value);
|
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) {
|
if (glucose[activeDate][moment] !== val) {
|
||||||
glucose[activeDate][moment] = val;
|
glucose[activeDate][moment] = val;
|
||||||
saveGlucoseToStorage();
|
saveGlucoseToStorage();
|
||||||
renderGlucoseSection();
|
updateGlucoseUI(moment);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enter key also saves
|
|
||||||
input.addEventListener('keydown', (e) => {
|
input.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
input.blur();
|
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 =====
|
// ===== Daily Carb Limit =====
|
||||||
const DEFAULT_CARB_LIMIT = 175; // grams per day
|
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
|
// Progress bar
|
||||||
renderProgressBar(totaal);
|
renderProgressBar(totaal);
|
||||||
|
|
||||||
// Glucose section
|
// Eating moments — prepend nuchter card, then eetmoment cards with inline glucose
|
||||||
renderGlucoseSection();
|
|
||||||
|
|
||||||
// Eating moments
|
|
||||||
const listEl = document.getElementById('eetmomentList');
|
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 items = dayData[moment.id] || [];
|
||||||
const momentKh = berekenEetmomentKh(activeDate, moment.id);
|
const momentKh = berekenEetmomentKh(activeDate, moment.id);
|
||||||
const roundedKh = Math.round(momentKh);
|
const roundedKh = Math.round(momentKh);
|
||||||
|
|
@ -3275,6 +3360,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const cardOverLimit = overLimit && momentKh > 0;
|
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 `
|
return `
|
||||||
<li class="eetmoment-card${cardOverLimit ? ' over-limit' : ''}" style="--i:${mi}">
|
<li class="eetmoment-card${cardOverLimit ? ' over-limit' : ''}" style="--i:${mi}">
|
||||||
|
|
@ -3284,9 +3372,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
<span class="eetmoment-name">${moment.name}</span>
|
<span class="eetmoment-name">${moment.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="eetmoment-kh">${roundedKh > 0 ? formatKh(roundedKh) + ' g' : '—'}</span>
|
<span class="eetmoment-kh">${roundedKh > 0 ? formatKh(roundedKh) + ' g' : '—'}</span>
|
||||||
<span style="font-size:0.8rem;color:var(--text-muted)">${items.length > 0 ? '<i class="fas fa-chevron-down"></i>' : ''}</span>
|
<span style="font-size:0.8rem;color:var(--text-muted)">${items.length > 0 ? '<i class=\"fas fa-chevron-down\"></i>' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
${items.length > 0 ? `<ul class="eetmoment-items">${itemsHtml}</ul>` : ''}
|
${items.length > 0 ? `<ul class="eetmoment-items">${itemsHtml}</ul>` : ''}
|
||||||
|
${inlineGlucoseHtml}
|
||||||
<div class="eetmoment-add-btn" data-moment="${moment.id}">
|
<div class="eetmoment-add-btn" data-moment="${moment.id}">
|
||||||
<span>+</span> Voedingsmiddel toevoegen
|
<span>+</span> Voedingsmiddel toevoegen
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -3379,6 +3468,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
||||||
// Empty state
|
// Empty state
|
||||||
const hasItems = EETMOMENTEN.some(m => (dayData[m.id] || []).length > 0);
|
const hasItems = EETMOMENTEN.some(m => (dayData[m.id] || []).length > 0);
|
||||||
document.getElementById('diaryEmpty').classList.toggle('hidden', hasItems);
|
document.getElementById('diaryEmpty').classList.toggle('hidden', hasItems);
|
||||||
|
|
||||||
|
// Attach glucose input handlers (nuchter card + inline glucose)
|
||||||
|
attachGlucoseInputHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDetail(item) {
|
function showDetail(item) {
|
||||||
|
|
|
||||||
|
|
@ -3,38 +3,41 @@ const { test, expect } = require('@playwright/test');
|
||||||
const BASE = 'https://eetdagboek.vantwout.dev';
|
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 */
|
/** Navigate to diary tab and wait for render */
|
||||||
async function openDiary(page) {
|
async function openDiary(page) {
|
||||||
await page.locator('#navDagboek').click();
|
await page.locator('#navDagboek').click();
|
||||||
await page.waitForTimeout(600);
|
await page.waitForTimeout(600);
|
||||||
// Wait for glucose section to render
|
// Wait for nuchter card to render
|
||||||
await expect(page.locator('.glucose-section')).toBeVisible({ timeout: 5000 });
|
await expect(page.locator('.nuchter-card')).toBeVisible({ timeout: 5000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fill a glucose input and trigger save-on-blur */
|
/** Fill a glucose input and trigger save-on-blur */
|
||||||
async function setGlucoseValue(page, momentKey, value) {
|
async function setGlucoseValue(page, momentKey, value) {
|
||||||
const input = page.locator(`input.glucose-input[data-moment="${momentKey}"]`);
|
const input = page.locator(`input.glucose-input[data-moment="${momentKey}"]`);
|
||||||
await input.fill(String(value));
|
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());
|
await input.evaluate(el => el.blur());
|
||||||
// Wait for re-render to complete
|
// Wait for re-render to complete
|
||||||
await page.waitForTimeout(400);
|
await page.waitForTimeout(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the parent .glucose-item for a given moment */
|
/** Get the parent glucose container for a given moment */
|
||||||
function glucoseItem(page, momentKey) {
|
function glucoseContainer(page, momentKey) {
|
||||||
return page.locator(`input.glucose-input[data-moment="${momentKey}"]`).locator('..').locator('..');
|
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 */
|
/** Get status text element for a given moment */
|
||||||
function statusText(page, momentKey) {
|
function statusText(page, momentKey) {
|
||||||
return page.locator(`input.glucose-input[data-moment="${momentKey}"]`)
|
if (momentKey === 'nuchter') {
|
||||||
.locator('..') // .glucose-input-wrapper
|
return page.locator('.nuchter-card .glucose-status-text');
|
||||||
.locator('..') // .glucose-item
|
}
|
||||||
.locator('.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 page.goto(BASE);
|
||||||
await openDiary(page);
|
await openDiary(page);
|
||||||
|
|
||||||
// Section structure
|
// Nuchter card structure
|
||||||
await expect(page.locator('.glucose-section')).toBeVisible();
|
await expect(page.locator('.nuchter-card')).toBeVisible();
|
||||||
await expect(page.locator('.glucose-title')).toHaveText('Glucose');
|
await expect(page.locator('.nuchter-card-label')).toHaveText('Nuchter (voor ontbijt)');
|
||||||
await expect(page.locator('.glucose-unit-label')).toHaveText('mmol/L');
|
await expect(page.locator('.nuchter-card-range')).toHaveText('< 5.4');
|
||||||
await expect(page.locator('.glucose-header .glucose-icon i')).toBeVisible();
|
|
||||||
|
|
||||||
// 4 grid items
|
// Inline glucose in eetmoment cards (Ontbijt, Lunch, Avondeten)
|
||||||
const items = page.locator('.glucose-item');
|
const moments = [
|
||||||
await expect(items).toHaveCount(4);
|
{ 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
|
// 3 inline glucose items should exist
|
||||||
await expect(items.nth(0).locator('.glucose-item-label')).toHaveText('Nuchter');
|
const inlineItems = page.locator('.glucose-inline');
|
||||||
await expect(items.nth(1).locator('.glucose-item-label')).toHaveText('Ontbijt');
|
await expect(inlineItems).toHaveCount(3);
|
||||||
await expect(items.nth(2).locator('.glucose-item-label')).toHaveText('Lunch');
|
|
||||||
await expect(items.nth(3).locator('.glucose-item-label')).toHaveText('Avondeten');
|
|
||||||
|
|
||||||
// Target ranges
|
for (let i = 0; i < moments.length; i++) {
|
||||||
await expect(items.nth(0).locator('.glucose-item-range')).toHaveText('< 5.4');
|
const m = moments[i];
|
||||||
await expect(items.nth(1).locator('.glucose-item-range')).toHaveText('5.4\u20136.7');
|
const container = page.locator(`.glucose-inline[data-moment-key="${m.key}"]`);
|
||||||
await expect(items.nth(2).locator('.glucose-item-range')).toHaveText('5.4\u20136.7');
|
await expect(container.locator('.glucose-inline-label')).toHaveText(m.label);
|
||||||
await expect(items.nth(3).locator('.glucose-item-range')).toHaveText('5.4\u20136.7');
|
await expect(container.locator('.glucose-inline-range')).toHaveText(m.range);
|
||||||
|
}
|
||||||
|
|
||||||
// Input attributes
|
// Input attributes — check 4 inputs
|
||||||
const moments = ['nuchter', 'naOntbijt', 'naLunch', 'naAvondeten'];
|
const inputMoments = ['nuchter', 'naOntbijt', 'naLunch', 'naAvondeten'];
|
||||||
for (let i = 0; i < 4; i++) {
|
for (const moment of inputMoments) {
|
||||||
const input = items.nth(i).locator('.glucose-input');
|
const input = page.locator(`input.glucose-input[data-moment="${moment}"]`);
|
||||||
await expect(input).toHaveAttribute('data-moment', moments[i]);
|
|
||||||
await expect(input).toHaveAttribute('step', '0.1');
|
await expect(input).toHaveAttribute('step', '0.1');
|
||||||
await expect(input).toHaveAttribute('min', '0');
|
await expect(input).toHaveAttribute('min', '0');
|
||||||
await expect(input).toHaveAttribute('max', '30');
|
await expect(input).toHaveAttribute('max', '30');
|
||||||
|
|
@ -90,11 +93,10 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent
|
||||||
|
|
||||||
await setGlucoseValue(page, 'nuchter', 5.1);
|
await setGlucoseValue(page, 'nuchter', 5.1);
|
||||||
|
|
||||||
// Check the item class and status
|
const container = glucoseContainer(page, 'nuchter');
|
||||||
const item = glucoseItem(page, 'nuchter');
|
await expect(container).toHaveClass(/glucose-good/);
|
||||||
await expect(item).toHaveClass(/glucose-good/);
|
await expect(container.locator('.glucose-status-icon')).toContainText('\u2705'); // ✅
|
||||||
await expect(item.locator('.glucose-status-icon')).toContainText('\u2705'); // ✅
|
await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range');
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 3. Nuchter ≥ 5.4 (rood) ----
|
// ---- 3. Nuchter ≥ 5.4 (rood) ----
|
||||||
|
|
@ -105,9 +107,9 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent
|
||||||
|
|
||||||
await setGlucoseValue(page, 'nuchter', 5.4);
|
await setGlucoseValue(page, 'nuchter', 5.4);
|
||||||
|
|
||||||
const item = glucoseItem(page, 'nuchter');
|
const container = glucoseContainer(page, 'nuchter');
|
||||||
await expect(item).toHaveClass(/glucose-bad/);
|
await expect(container).toHaveClass(/glucose-bad/);
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Te hoog');
|
await expect(container.locator('.glucose-status-text')).toHaveText('Te hoog');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 4. Postprandiaal < 5.4 (rood/te laag) ----
|
// ---- 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);
|
await setGlucoseValue(page, 'naOntbijt', 5.3);
|
||||||
|
|
||||||
const item = glucoseItem(page, 'naOntbijt');
|
const container = glucoseContainer(page, 'naOntbijt');
|
||||||
await expect(item).toHaveClass(/glucose-bad/);
|
await expect(container).toHaveClass(/glucose-bad/);
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Te laag');
|
await expect(container.locator('.glucose-status-text')).toHaveText('Te laag');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 5. Postprandiaal binnen range (groen) ----
|
// ---- 5. Postprandiaal binnen range (groen) ----
|
||||||
|
|
@ -131,9 +133,9 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent
|
||||||
|
|
||||||
await setGlucoseValue(page, 'naOntbijt', 6.0);
|
await setGlucoseValue(page, 'naOntbijt', 6.0);
|
||||||
|
|
||||||
const item = glucoseItem(page, 'naOntbijt');
|
const container = glucoseContainer(page, 'naOntbijt');
|
||||||
await expect(item).toHaveClass(/glucose-good/);
|
await expect(container).toHaveClass(/glucose-good/);
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
|
await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 6. Postprandiaal > 6.7 (rood/te hoog) ----
|
// ---- 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);
|
await setGlucoseValue(page, 'naLunch', 6.8);
|
||||||
|
|
||||||
const item = glucoseItem(page, 'naLunch');
|
const container = glucoseContainer(page, 'naLunch');
|
||||||
await expect(item).toHaveClass(/glucose-bad/);
|
await expect(container).toHaveClass(/glucose-bad/);
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Te hoog');
|
await expect(container.locator('.glucose-status-text')).toHaveText('Te hoog');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 7. Leeg veld — grijze 'nog meten' indicator ----
|
// ---- 7. Leeg veld — grijze 'nog meten' indicator ----
|
||||||
|
|
@ -155,31 +157,24 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent
|
||||||
await page.goto(BASE);
|
await page.goto(BASE);
|
||||||
await openDiary(page);
|
await openDiary(page);
|
||||||
|
|
||||||
// All 4 items should start empty
|
// Nuchter card should start empty
|
||||||
const items = page.locator('.glucose-item');
|
const nuchter = glucoseContainer(page, 'nuchter');
|
||||||
const count = await items.count();
|
await expect(nuchter).toHaveClass(/glucose-empty/);
|
||||||
expect(count).toBe(4);
|
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++) {
|
// Inline items should start empty
|
||||||
await expect(items.nth(i)).toHaveClass(/glucose-empty/);
|
const inlineMoments = ['naOntbijt', 'naLunch', 'naAvondeten'];
|
||||||
}
|
for (const moment of inlineMoments) {
|
||||||
|
const container = glucoseContainer(page, moment);
|
||||||
const statusTexts = page.locator('.glucose-status-text');
|
await expect(container).toHaveClass(/glucose-empty/);
|
||||||
const texts = await statusTexts.allTextContents();
|
await expect(container.locator('.glucose-status-text')).toHaveText('Nog meten');
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All inputs should have empty value
|
// All inputs should have empty value
|
||||||
const inputs = page.locator('.glucose-input');
|
const inputs = page.locator('.glucose-input');
|
||||||
const inputCount = await inputs.count();
|
const inputCount = await inputs.count();
|
||||||
|
expect(inputCount).toBe(4);
|
||||||
for (let i = 0; i < inputCount; i++) {
|
for (let i = 0; i < inputCount; i++) {
|
||||||
await expect(inputs.nth(i)).toHaveValue('');
|
await expect(inputs.nth(i)).toHaveValue('');
|
||||||
}
|
}
|
||||||
|
|
@ -195,20 +190,20 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent
|
||||||
await setGlucoseValue(page, 'nuchter', 5.1);
|
await setGlucoseValue(page, 'nuchter', 5.1);
|
||||||
|
|
||||||
// Verify it's shown correctly
|
// Verify it's shown correctly
|
||||||
let item = glucoseItem(page, 'nuchter');
|
let container = glucoseContainer(page, 'nuchter');
|
||||||
await expect(item).toHaveClass(/glucose-good/);
|
await expect(container).toHaveClass(/glucose-good/);
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
|
await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range');
|
||||||
await expect(item.locator('.glucose-input')).toHaveValue('5.1');
|
await expect(container.locator('.glucose-input')).toHaveValue('5.1');
|
||||||
|
|
||||||
// Reload the page
|
// Reload the page
|
||||||
await page.goto(BASE);
|
await page.goto(BASE);
|
||||||
await openDiary(page);
|
await openDiary(page);
|
||||||
|
|
||||||
// Value should still be there
|
// Value should still be there
|
||||||
item = glucoseItem(page, 'nuchter');
|
container = glucoseContainer(page, 'nuchter');
|
||||||
await expect(item).toHaveClass(/glucose-good/);
|
await expect(container).toHaveClass(/glucose-good/);
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
|
await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range');
|
||||||
await expect(item.locator('.glucose-input')).toHaveValue('5.1');
|
await expect(container.locator('.glucose-input')).toHaveValue('5.1');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 9. Datum-navigatie — andere datum andere waarden ----
|
// ---- 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);
|
await setGlucoseValue(page, 'nuchter', 5.1);
|
||||||
|
|
||||||
// Verify today shows the value
|
// Verify today shows the value
|
||||||
let item = glucoseItem(page, 'nuchter');
|
let container = glucoseContainer(page, 'nuchter');
|
||||||
await expect(item).toHaveClass(/glucose-good/);
|
await expect(container).toHaveClass(/glucose-good/);
|
||||||
|
|
||||||
// Navigate to tomorrow
|
// Navigate to tomorrow
|
||||||
await page.locator('#dayNext').click();
|
await page.locator('#dayNext').click();
|
||||||
await page.waitForTimeout(600);
|
await page.waitForTimeout(600);
|
||||||
|
|
||||||
// Tomorrow should have all empty glucose fields
|
// Tomorrow should have all empty glucose fields
|
||||||
item = glucoseItem(page, 'nuchter');
|
container = glucoseContainer(page, 'nuchter');
|
||||||
await expect(item).toHaveClass(/glucose-empty/);
|
await expect(container).toHaveClass(/glucose-empty/);
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Nog meten');
|
await expect(container.locator('.glucose-status-text')).toHaveText('Nog meten');
|
||||||
await expect(item.locator('.glucose-input')).toHaveValue('');
|
await expect(container.locator('.glucose-input')).toHaveValue('');
|
||||||
|
|
||||||
// Also check the other moments are empty too
|
// Also check the other moments are empty too
|
||||||
for (const moment of ['naOntbijt', 'naLunch', 'naAvondeten']) {
|
for (const moment of ['naOntbijt', 'naLunch', 'naAvondeten']) {
|
||||||
const otherItem = glucoseItem(page, moment);
|
const otherContainer = glucoseContainer(page, moment);
|
||||||
await expect(otherItem).toHaveClass(/glucose-empty/);
|
await expect(otherContainer).toHaveClass(/glucose-empty/);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate back to today
|
// Navigate back to today
|
||||||
|
|
@ -245,10 +240,10 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent
|
||||||
await page.waitForTimeout(600);
|
await page.waitForTimeout(600);
|
||||||
|
|
||||||
// Today's value should still be there
|
// Today's value should still be there
|
||||||
item = glucoseItem(page, 'nuchter');
|
container = glucoseContainer(page, 'nuchter');
|
||||||
await expect(item).toHaveClass(/glucose-good/);
|
await expect(container).toHaveClass(/glucose-good/);
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
|
await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range');
|
||||||
await expect(item.locator('.glucose-input')).toHaveValue('5.1');
|
await expect(container.locator('.glucose-input')).toHaveValue('5.1');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- 10. Decimale invoer — step 0.1 ----
|
// ---- 10. Decimale invoer — step 0.1 ----
|
||||||
|
|
@ -263,18 +258,18 @@ test.describe('Glucose-metingen — invoer, validatie, kleurcodering, persistent
|
||||||
|
|
||||||
// Enter 5.5 (decimal)
|
// Enter 5.5 (decimal)
|
||||||
await setGlucoseValue(page, 'nuchter', 5.5);
|
await setGlucoseValue(page, 'nuchter', 5.5);
|
||||||
let item = glucoseItem(page, 'nuchter');
|
let container = glucoseContainer(page, 'nuchter');
|
||||||
await expect(item.locator('.glucose-input')).toHaveValue('5.5');
|
await expect(container.locator('.glucose-input')).toHaveValue('5.5');
|
||||||
// 5.5 ≥ 5.4 for nuchter → should be bad/Te hoog
|
// 5.5 ≥ 5.4 for nuchter → should be bad/Te hoog
|
||||||
await expect(item).toHaveClass(/glucose-bad/);
|
await expect(container).toHaveClass(/glucose-bad/);
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Te hoog');
|
await expect(container.locator('.glucose-status-text')).toHaveText('Te hoog');
|
||||||
|
|
||||||
// Now test a postprandial decimal value
|
// Now test a postprandial decimal value
|
||||||
await setGlucoseValue(page, 'naOntbijt', 6.2);
|
await setGlucoseValue(page, 'naOntbijt', 6.2);
|
||||||
item = glucoseItem(page, 'naOntbijt');
|
container = glucoseContainer(page, 'naOntbijt');
|
||||||
await expect(item.locator('.glucose-input')).toHaveValue('6.2');
|
await expect(container.locator('.glucose-input')).toHaveValue('6.2');
|
||||||
await expect(item).toHaveClass(/glucose-good/);
|
await expect(container).toHaveClass(/glucose-good/);
|
||||||
await expect(item.locator('.glucose-status-text')).toHaveText('Binnen range');
|
await expect(container.locator('.glucose-status-text')).toHaveText('Binnen range');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue