- FIX #70 : generateForecastDoses garde !isActive — un traitement archivé à Posologie ne projette plus de créneaux ; l'horizon du chip Prévision (chart.js) ne considère que les Posologies ACTIVES. §6.bis inchangé : l'historique reste simulé, seul le FUTUR s'arrête. 3 tests miroirs. - Page Doses : routeGlyph (💊💉💧🩹🧴, miroir routeIcon) + encarts « prise de sang le même jour » 🧪↑/🧪↓ (labMarkersForDose, miroir) ; i18n FR/EN ; 10 tests doses-extras.test.js (⚠️ makeDose(ts, amount, overrides)). 194 tests + E2E verts (check.sh). WEB_VERSION alignée sur 1.12.0.
150 lines
7.1 KiB
JavaScript
150 lines
7.1 KiB
JavaScript
/**
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
* Écran Doses (portage de `ui/screens/DosesScreen.kt`).
|
|
*
|
|
* - en-tête « temps sous THS » (v1.3.1) : depuis la 1re prise, jours
|
|
* totaux + décomposition mois(30 j)/jours — ⚠️ la string hrt_duration a
|
|
* TROIS placeholders (mois, jours, total) : les trois sont passés (bug
|
|
* Android #47 = crash MissingFormatArgumentException) ;
|
|
* - liste groupée par jour (desc) ;
|
|
* - Δ jours depuis la dose PRÉCÉDENTE du MÊME traitement (colonne
|
|
* « Interval (d) » du .ods) ;
|
|
* - tap ligne → édition (TOUS les traitements proposés — §6.bis) ;
|
|
* - suppression avec confirmation.
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
*/
|
|
|
|
import { store } from '../data/store.js';
|
|
import { routeGlyph, routeLabelKey } from '../data/models.js';
|
|
import { t } from '../util/i18n.js';
|
|
import { formatDose, formatTime, formatLongDate, formatISODate, HrtDuration, labMarkersForDose } from '../util/format.js';
|
|
import { el, assistChip, confirmDialog, fab } from './components.js';
|
|
import { openDoseDialog } from './dialogs.js';
|
|
|
|
const DAY_MS = 86400000;
|
|
|
|
/**
|
|
* Intervalle (en jours) entre chaque dose et la dose précédente du MÊME
|
|
* traitement (miroir intervalsByDoseId) — clé = id de la dose.
|
|
*
|
|
* @param {object[]} doseLogs
|
|
* @returns {Map<number,number>} id → jours (décimaux)
|
|
*/
|
|
export function intervalsByDoseId(doseLogs) {
|
|
const byTreatment = new Map();
|
|
for (const d of doseLogs.slice().sort((a, b) => a.timestamp - b.timestamp)) {
|
|
if (!byTreatment.has(d.treatmentId)) byTreatment.set(d.treatmentId, []);
|
|
byTreatment.get(d.treatmentId).push(d);
|
|
}
|
|
const out = new Map();
|
|
for (const logs of byTreatment.values()) {
|
|
for (let i = 1; i < logs.length; i++) {
|
|
out.set(logs[i].id, (logs[i].timestamp - logs[i - 1].timestamp) / DAY_MS);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Rend l'écran Doses.
|
|
* @param {HTMLElement} container
|
|
*/
|
|
export function renderDoses(container) {
|
|
const doseLogs = store.getDoseLogs().slice().sort((a, b) => b.timestamp - a.timestamp);
|
|
const treatments = store.getTreatments(); // TOUS (édition d'une dose inactivée OK)
|
|
const treatmentMap = new Map(treatments.map((tr) => [tr.id, tr]));
|
|
const intervals = intervalsByDoseId(doseLogs);
|
|
|
|
const col = el('div', {});
|
|
container.appendChild(col);
|
|
|
|
// ── En-tête « temps sous THS » (v1.3.1) ──────────────────────────────────
|
|
if (doseLogs.length > 0) {
|
|
const first = Math.min(...doseLogs.map((d) => d.timestamp));
|
|
const card = el('div', { class: 'card primary' });
|
|
card.appendChild(el('div', { style: 'font-weight:600;' }, t('hrt_since', formatLongDate(first))));
|
|
const totalDays = HrtDuration.daysSince(first, Date.now());
|
|
if (totalDays > 0) {
|
|
const [months, days] = HrtDuration.monthsAndDays(totalDays);
|
|
// ⚠️ hrt_duration a TROIS placeholders (%1 mois, %2 jours, %3 total) :
|
|
// passer aussi le total, sinon crash Android #47 (épinglé ici aussi).
|
|
card.appendChild(el('div', {},
|
|
months > 0 ? t('hrt_duration', months, days, totalDays) : t('hrt_duration_days_only', days)));
|
|
}
|
|
col.appendChild(card);
|
|
}
|
|
|
|
if (doseLogs.length === 0) {
|
|
col.appendChild(el('p', {}, t('no_data')));
|
|
container.appendChild(fab(() => openDoseDialog({ treatments: treatments.filter((x) => x.isActive), preselected: null, existing: null })));
|
|
return;
|
|
}
|
|
|
|
// ── Groupement par jour (desc) ───────────────────────────────────────────
|
|
const grouped = new Map();
|
|
for (const log of doseLogs) {
|
|
const key = formatISODate(log.timestamp);
|
|
if (!grouped.has(key)) grouped.set(key, []);
|
|
grouped.get(key).push(log);
|
|
}
|
|
|
|
for (const [dateKey, logs] of grouped) {
|
|
col.appendChild(el('div', { class: 'group-header' }, dateKey));
|
|
for (const log of logs) {
|
|
const tr = treatmentMap.get(log.treatmentId);
|
|
// v1.12.0 : marqueurs « prise de sang le même jour » (↑ avant / ↓
|
|
// après) — miroir labMarkersForDose de DosesScreen.kt
|
|
const markers = labMarkersForDose(store.getLabResults(), log.timestamp);
|
|
const item = el('div', { class: 'list-item', onclick: () => openDoseDialog({ treatments, preselected: null, existing: log }) });
|
|
// v1.12.0 : GLYPHE de forme de prise à gauche (💊 oral, 💉 injection
|
|
// IM/SC, 💧 gel, 🩹 patch, 🧴 autre — cf routeGlyph dans models.js)
|
|
item.appendChild(el('span', {
|
|
class: 'sub',
|
|
style: 'align-self:center;font-size:1.1em;',
|
|
title: tr ? t(routeLabelKey(tr.route)) : '',
|
|
}, routeGlyph(tr ? tr.route : null)));
|
|
const main = el('div', { class: 'main' });
|
|
main.appendChild(el('div', { class: 'title' },
|
|
`${tr ? tr.name : '?'} · ${formatDose(log.doseAmount)} ${tr ? tr.doseUnit : ''}`));
|
|
const sub = el('div', { class: 'sub' });
|
|
sub.appendChild(el('span', {}, formatTime(log.timestamp)));
|
|
// Intervalle depuis la dose précédente du même traitement
|
|
const iv = intervals.get(log.id);
|
|
if (iv !== undefined) sub.appendChild(el('span', {}, t('interval_value', iv.toFixed(1))));
|
|
// Override d'ester de cette injection (chip, comme Android)
|
|
if (log.esterType && log.esterType !== '' && log.esterType !== 'NONE') {
|
|
sub.appendChild(assistChip(log.esterType));
|
|
}
|
|
if (log.notes && log.notes.trim() !== '') sub.appendChild(el('span', {}, log.notes));
|
|
main.appendChild(sub);
|
|
item.appendChild(main);
|
|
const side = el('div', { class: 'row', style: 'align-items:center;gap:2px;' });
|
|
// Encart latéral « prise de sang le même jour » (title = phrase
|
|
// complète pour l'accessibilité, miroir des contentDescription)
|
|
if (markers.before) {
|
|
side.appendChild(el('span', {
|
|
class: 'sub', style: 'color:var(--accent,#d6589e);',
|
|
title: t('lab_before_dose'),
|
|
}, '🧪↑'));
|
|
}
|
|
if (markers.after) {
|
|
side.appendChild(el('span', {
|
|
class: 'sub', style: 'color:var(--accent,#d6589e);',
|
|
title: t('lab_after_dose'),
|
|
}, '🧪↓'));
|
|
}
|
|
const del = el('button', { class: 'icon-btn', 'aria-label': t('delete') }, '🗑');
|
|
del.addEventListener('click', (e) => {
|
|
e.stopPropagation(); // ne pas déclencher l'édition
|
|
confirmDialog(t('delete'), t('confirm_delete'), () => store.deleteDoseLog(log.id));
|
|
});
|
|
side.appendChild(del);
|
|
item.appendChild(side);
|
|
col.appendChild(item);
|
|
}
|
|
}
|
|
|
|
// FAB : création (traitements ACTIFS uniquement — miroir DosesScreen)
|
|
container.appendChild(fab(() => openDoseDialog({ treatments: treatments.filter((x) => x.isActive), preselected: null, existing: null })));
|
|
}
|