Siphonight 4bdcd52970 v1.12.0 (web) : fix #70 (inactifs sans projection) + page Doses (glyphes de forme, marqueurs prise de sang) — miroir Android
- 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.
2026-09-29 23:24:18 +02:00

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 })));
}