Corrigé (audit) : - cutoffHours : garde hasModel pour ESE (parité Kotlin) — un backup importé ESE + ester non couvert levait TypeError → écran vide - min/max en une passe : les spreads Math.min(...allPoints) levaient RangeError au-delà de ~1e5 arguments (Nuage × zoom max) et allouaient par frame - init MCMC non bloquante : data-ready garantit désormais « asset chargé » Perf (portage v1.8.2 Android, fin de la dérive de miroir) : - prepareE2Context/e2AtCtx : doses groupées, cutoffs précalculés, Bateman paresseux ; computeCurve ne recalcule plus cutoffHours par point - résultat identique (175 tests verts) E2E : version lue via data-version (anti-bug v1.9.3) Docs/commentaires : pk-engine (ESE analytique), pk-calibration (JSDoc arrondi), dialogs (ESE=6), lab-timing (import mort, JSDoc orpheline), home (COLORS), pk-profile-store (test-only), pk_profiles.json exclu du zip (export-ignore, −548 Ko)
204 lines
10 KiB
JavaScript
204 lines
10 KiB
JavaScript
/**
|
||
* ─────────────────────────────────────────────────────────────────────────────
|
||
* Écran Accueil (portage de `ui/screens/HomeScreen.kt`).
|
||
*
|
||
* - carte « niveau actuel » : E2 ≈ X pg/mL + T ≈ Y ng/mL + delta vs 6 h
|
||
* (v1.4.3, fix #53 : point le plus PROCHE de −6 h via pointHoursBefore) ;
|
||
* - cartes d'alerte si un seuil est franchi (v1.4.2, évaluées sur le
|
||
* DERNIER point de courbe = le niveau affiché) ;
|
||
* - carte « prochaine dose » : heures/min sous 24 h, JOURS au-delà (v1.4.1),
|
||
* grille Posologie via le moteur (fix #52) ;
|
||
* - chips de log rapide (traitements ACTIFS uniquement — l'inactivation ne
|
||
* retire JAMAIS un traitement de la simulation, seulement de la saisie) ;
|
||
* - mini-chart 24 h cliquable → écran Graphiques.
|
||
*
|
||
* ⚠️ La simulation reçoit TOUS les traitements (actifs ET inactifs) : bug
|
||
* v1.2.4 — « isActive » est un drapeau administratif, jamais un filtre de
|
||
* données (cf régression #3).
|
||
* ─────────────────────────────────────────────────────────────────────────────
|
||
*/
|
||
|
||
import { computeCurve, autoCalibrated, nextReminderFireFor, Alerts, TConfig, pointHoursBefore, nextBloodDrawRecommendation } from '../pk/index.js';
|
||
import { store } from '../data/store.js';
|
||
import { t } from '../util/i18n.js';
|
||
import { formatDose, formatValue, formatTime, formatWeekdayTime, formatDateTime, HrtDuration } from '../util/format.js';
|
||
import { el } from './components.js';
|
||
import { drawChart, COLORS } from './chart-canvas.js';
|
||
import { openDoseDialog } from './dialogs.js';
|
||
|
||
const HOUR_MS = 3600000;
|
||
|
||
/**
|
||
* Rend l'écran accueil dans [container].
|
||
* @param {HTMLElement} container
|
||
* @param {object} ctx { nowMs, openChart, openTreatment }
|
||
*/
|
||
export function renderHome(container, ctx) {
|
||
const treatments = store.getTreatments(); // TOUS (actifs + inactifs)
|
||
const activeTreatments = treatments.filter((tr) => tr.isActive);
|
||
const doseLogs = store.getDoseLogs();
|
||
const labResults = store.getLabResults();
|
||
const tConfig = new TConfig(store.getTConfig().base, store.getTConfig().floor, store.getTConfig().k);
|
||
const settings = store.getSettings();
|
||
const nowMs = ctx.nowMs;
|
||
|
||
// Auto-calibration (v1.2.1) : facteurs PAR PÉRIODE D'ESTER + k T, recalculés
|
||
// à la volée depuis les labs. null = option désactivée (échelles stockées).
|
||
// Home calibre avec le modèle STOCKÉ (pas d'override — le graphique, lui,
|
||
// calcule une calibration PAR modèle affiché, fix #60).
|
||
let effectiveAuto = null;
|
||
if (settings.autoCalibrate) {
|
||
effectiveAuto = autoCalibrated(treatments, doseLogs, labResults, tConfig, null);
|
||
}
|
||
const effTreatments = effectiveAuto ? effectiveAuto.treatments : treatments;
|
||
const effTConfig = effectiveAuto ? effectiveAuto.tConfig : tConfig;
|
||
const scalePerEster = effectiveAuto ? effectiveAuto.esterScales : null;
|
||
const tKPerEster = effectiveAuto ? effectiveAuto.tKPerEster : null;
|
||
|
||
// Courbe 24 h (grille horaire — le mini-chart de l'accueil n'a pas besoin
|
||
// du pas adaptatif du graphique complet)
|
||
const curve = computeCurve(
|
||
effTreatments, doseLogs, nowMs - 24 * HOUR_MS, nowMs, HOUR_MS, effTConfig,
|
||
{ scalePerEster, tKPerEster },
|
||
);
|
||
|
||
const col = el('div', {});
|
||
|
||
// ── Carte « niveau actuel » ──────────────────────────────────────────────
|
||
const now = curve[curve.length - 1] || null;
|
||
// v1.4.3 (fix #53) : le point de comparaison « il y a 6 h » est celui le
|
||
// plus PROCHE de −6 h (l'ancien code prenait le PREMIER point de la
|
||
// fenêtre 24 h → le delta affiché était en réalité un delta 24 h).
|
||
const sixHAgo = pointHoursBefore(curve, 6.0);
|
||
const levelCard = el('div', { class: 'card primary' });
|
||
levelCard.appendChild(el('div', { class: 'label' }, t('current_level')));
|
||
if (!now) {
|
||
levelCard.appendChild(el('div', {}, t('no_data_short')));
|
||
} else {
|
||
levelCard.appendChild(el('div', { class: 'row' }, [
|
||
el('span', { class: 'big-number' }, `≈ ${formatValue(now.e2)}`),
|
||
el('span', { class: 'unit' }, ' pg/mL'),
|
||
el('span', { class: 't-inline' }, ` T ≈ ${formatValue(now.t)} ng/mL`),
|
||
]));
|
||
if (sixHAgo) {
|
||
const delta = now.e2 - sixHAgo.e2;
|
||
const arrow = delta >= 0 ? '↗' : '↘';
|
||
levelCard.appendChild(el('div', { class: delta >= 0 ? 'delta-up' : 'delta-down' },
|
||
t('delta_6h', arrow, formatValue(Math.abs(delta)))));
|
||
}
|
||
}
|
||
col.appendChild(levelCard);
|
||
|
||
// ── Cartes d'alerte (v1.4.2) : taux ESTIMÉ actuel vs seuils configurés ────
|
||
if (now) {
|
||
const thresholds = new Alerts.Thresholds(
|
||
settings.alertE2High, settings.alertE2Low, settings.alertTHigh, settings.alertTLow,
|
||
);
|
||
const alerts = Alerts.evaluateAll(now.e2, now.t, thresholds);
|
||
if (alerts.length) {
|
||
const alertCard = el('div', { class: 'card error' });
|
||
alertCard.appendChild(el('h3', {}, t('alert_card_title')));
|
||
for (const a of alerts) {
|
||
alertCard.appendChild(el('div', {},
|
||
a.level === 'HIGH'
|
||
? t('alert_high', a.marker, formatDose(a.value), a.unit, formatDose(a.limit))
|
||
: t('alert_low', a.marker, formatDose(a.value), a.unit, formatDose(a.limit))));
|
||
}
|
||
alertCard.appendChild(el('div', { class: 'small' }, t('alert_disclaimer')));
|
||
col.appendChild(alertCard);
|
||
}
|
||
}
|
||
|
||
// ── Carte « prochaine dose » (grille Posologie via le moteur, fix #52) ───
|
||
const fires = [];
|
||
for (const tr of treatments) {
|
||
const fire = nextReminderFireFor(tr, doseLogs, nowMs);
|
||
if (fire !== null) fires.push([tr, fire]);
|
||
}
|
||
if (fires.length) {
|
||
fires.sort((a, b) => a[1] - b[1]);
|
||
const [nextTreatment, next] = fires[0];
|
||
const deltaMs = next - nowMs;
|
||
const h = Math.floor(deltaMs / HOUR_MS);
|
||
const m = Math.floor((deltaMs % HOUR_MS) / 60000);
|
||
const card = el('div', { class: 'card' });
|
||
card.appendChild(el('div', { class: 'label' }, t('next_dose')));
|
||
if (h >= 24) {
|
||
// v1.4.1 : au-delà de 24 h, afficher en JOURS + jour de semaine
|
||
const [days, remH] = HrtDuration.daysAndHours(h);
|
||
card.appendChild(el('div', { style: 'font-weight:600;' },
|
||
t('next_dose_days', days, remH, formatWeekdayTime(next), nextTreatment.name)));
|
||
} else if (h > 0) {
|
||
card.appendChild(el('div', { style: 'font-weight:600;' },
|
||
`${h}h${String(m).padStart(2, '0')} · ${formatTime(next)} (${nextTreatment.name})`));
|
||
} else {
|
||
card.appendChild(el('div', { style: 'font-weight:600;' },
|
||
`${m} min · ${formatTime(next)} (${nextTreatment.name})`));
|
||
}
|
||
col.appendChild(card);
|
||
}
|
||
|
||
// ── Recommandation de prochaine prise de sang (v1.8.1) ───────────────────
|
||
// Même calcul que la page Analyses (pk/LabTiming.js) — encart COMPACT sur
|
||
// l'accueil (demande v1.8.1) : creux daté + créneau associé + mention de
|
||
// stabilisation SEULEMENT si le régime n'était pas déjà stable (sinon le
|
||
// texte est identique à chaque ouverture). Caché si non calculable.
|
||
const labRec = nextBloodDrawRecommendation(treatments, doseLogs, labResults, nowMs);
|
||
if (labRec) {
|
||
const recCard = el('div', { class: 'card variant' });
|
||
recCard.appendChild(el('div', { class: 'label' }, t('labrec_section')));
|
||
recCard.appendChild(el('div', { style: 'font-weight:600;' },
|
||
t('labrec_when', formatWeekdayTime(labRec.troughMs))));
|
||
recCard.appendChild(el('div', {},
|
||
t('labrec_before_injection', labRec.ester, formatWeekdayTime(labRec.injectionMs))));
|
||
if (!labRec.wasAlreadyStabilized) {
|
||
recCard.appendChild(el('div', { class: 'legend-muted' },
|
||
t('labrec_not_stabilized', labRec.ester, formatDateTime(new Date(labRec.stabilizedAtMs)))));
|
||
}
|
||
col.appendChild(recCard);
|
||
}
|
||
|
||
// ── Log rapide : chips des traitements ACTIFS uniquement (saisie) ────────
|
||
const quickCard = el('div', { class: 'card variant' });
|
||
quickCard.appendChild(el('h3', {}, t('logged_today')));
|
||
const chipRow = el('div', { class: 'chip-row' });
|
||
for (const tr of activeTreatments) {
|
||
chipRow.appendChild(el('span', {
|
||
class: 'chip',
|
||
onclick: () => openDoseDialog({ treatments: activeTreatments, preselected: tr, existing: null }),
|
||
}, `${tr.name} · ${formatDose(tr.doseAmount)}${tr.doseUnit}`));
|
||
}
|
||
chipRow.appendChild(el('button', {
|
||
class: 'chip',
|
||
onclick: () => openDoseDialog({ treatments: activeTreatments, preselected: null, existing: null }),
|
||
}, '+'));
|
||
quickCard.appendChild(chipRow);
|
||
col.appendChild(quickCard);
|
||
|
||
// ── Mini-chart 24 h cliquable → écran Graphiques ─────────────────────────
|
||
const chartCard = el('div', { class: 'card', style: 'cursor:pointer;', onclick: ctx.openChart });
|
||
chartCard.appendChild(el('h3', {}, t('home_chart_title')));
|
||
const canvas = el('canvas', { class: 'chart', style: 'height:180px;' });
|
||
chartCard.appendChild(canvas);
|
||
// Labs limités à la fenêtre de la courbe (miroir filterInRange)
|
||
if (curve.length >= 2) {
|
||
const a = curve[0].timestamp;
|
||
const b = curve[curve.length - 1].timestamp;
|
||
const e2Labs = labResults.filter((l) => String(l.marker).toUpperCase() === 'E2' && l.timestamp >= a && l.timestamp <= b);
|
||
const tLabs = labResults.filter((l) => String(l.marker).toUpperCase() === 'T' && l.timestamp >= a && l.timestamp <= b);
|
||
// Dessin différé : le canvas doit être dans le DOM pour connaître sa taille
|
||
requestAnimationFrame(() => {
|
||
drawChart(canvas, [{ points: curve, e2Style: { color: COLORS.chartE2, dashed: false }, tStyle: { color: COLORS.chartT, dashed: false } }],
|
||
e2Labs, tLabs, { showT: true, showLabs: true, showExtrema: false, nowMs }, [], [], null);
|
||
});
|
||
}
|
||
chartCard.appendChild(el('div', { class: 'legend-text', style: `color:${COLORS.chartE2};` }, t('home_legend_e2')));
|
||
chartCard.appendChild(el('div', { class: 'legend-text', style: `color:${COLORS.chartT};` }, t('home_legend_t')));
|
||
chartCard.appendChild(el('div', { class: 'legend-muted' }, t('open_charts_hint')));
|
||
col.appendChild(chartCard);
|
||
|
||
col.appendChild(el('div', { class: 'hint' }, t('disclaimer')));
|
||
|
||
container.appendChild(col);
|
||
}
|