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.
123 lines
5.8 KiB
JavaScript
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 })));
|
|
}
|