Siphonight 491efef8eb v1.4.10 — portage initial de l'app Android v1.4.10
Portage navigateur complet de l'app Android HormoneTrack v1.4.10 :
- moteur PK fidèle (Estrannaise tables ODS, Transfem Science V3C,
  WHSAH fit Mona, Bateman) — paramètres identiques, invariants Android
  préservés (fixes #19-#23, #35, #52-#62), 121 tests Node épinglés
- calibration par période d'ester et par modèle affiché (#60/#61)
- UI 6 écrans + éditeur, CurveChart Canvas (zoom/pan fractionnaire,
  prévision, pics/creux, fuseau configurable), rappels web
  (Notification API), seuils d'alerte, i18n FR/EN
- sauvegarde JSON v2 compatible Android bidirectionnelle (rétrocompat v1)
- 100 % local : localStorage, aucun serveur applicatif, aucune télémétrie
- processus : scripts/check.sh (syntaxe, i18n, tests, E2E navigateur,
  smoke HTTP), docs séparées (README + DEVELOPPEMENT + CHANGELOG)
- dépôt GIT SÉPARÉ de l'Android : historique 100 % propre, versions
  alignées sur l'Android porté, changelogs indépendants

Non porté (impossible dans un navigateur, documenté §12) : agenda
récurrent, notifications onglet fermé, montre.
2026-09-08 14:34:58 +02:00

123 lines
5.8 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 { t } from '../util/i18n.js';
import { formatDose, formatTime, formatLongDate, formatISODate, HrtDuration } 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);
const item = el('div', { class: 'list-item', onclick: () => openDoseDialog({ treatments, preselected: null, existing: log }) });
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 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));
});
item.appendChild(del);
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 })));
}