v1.9.6 (web) : la carte Accueil liste TOUTES les prochaines doses (triées) — miroir Android
- helper PUR nextDoseLineText dans format.js (miroir du Kotlin nextDoseLine) : « Xj Yh · sam. 26 10:00 (nom) » > 24 h, « XhMM » sinon, « N min » dans l'heure - home.js : une ligne par traitement, triées par prochaine prise, 1ʳᵉ en avant, titre pluriel « Prochaines doses » dès 2 lignes (i18n EN/FR) - 3 tests miroirs dans misc.test.js (178 verts + E2E) - WEB_VERSION 1.9.6 + CHANGELOG/README
This commit is contained in:
parent
be4d26b3e5
commit
048a76e540
@ -17,12 +17,12 @@
|
|||||||
> Fie-toi toujours à tes analyses de sang et aux consignes de ton
|
> Fie-toi toujours à tes analyses de sang et aux consignes de ton
|
||||||
> endocrinologue.
|
> endocrinologue.
|
||||||
|
|
||||||
- **Statut** : web v1.9.5 — parité fonctionnelle avec l'Android v1.9.5
|
- **Statut** : web v1.9.6 — parité fonctionnelle avec l'Android v1.9.6
|
||||||
(modèle Estrannaise analytique + nuage d'incertitude MCMC exclusif ESE ;
|
(modèle Estrannaise analytique + nuage d'incertitude MCMC exclusif ESE ;
|
||||||
presets des 6 esters injectables ESE ; l'auto-backup journalier Android
|
presets des 6 esters injectables ESE ; l'auto-backup journalier Android
|
||||||
reste structurellement non porté, cf §12 — l'export manuel couvre la
|
reste structurellement non porté, cf §12 — l'export manuel couvre la
|
||||||
donnée) ·
|
donnée) ·
|
||||||
**175 tests verts** · E2E navigateur ✅ · conteneur testé en
|
**178 tests verts** · E2E navigateur ✅ · conteneur testé en
|
||||||
configuration durcie ✅ · lint/i18n ✅
|
configuration durcie ✅ · lint/i18n ✅
|
||||||
- **Journal des versions web** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
- **Journal des versions web** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
|
||||||
- **Releases** : [gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases](https://gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases)
|
- **Releases** : [gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases](https://gitea.cloudyfy.fr/Siphonight/HormoneTrack-web/releases)
|
||||||
|
|||||||
@ -5,6 +5,20 @@
|
|||||||
> section la plus récente EN PREMIER ; ce fichier est lu par le dialog
|
> section la plus récente EN PREMIER ; ce fichier est lu par le dialog
|
||||||
> « Nouveautés » de l'app web (fetch `docs/CHANGELOG.md`).
|
> « Nouveautés » de l'app web (fetch `docs/CHANGELOG.md`).
|
||||||
|
|
||||||
|
## [1.9.6] — 2026-09-20
|
||||||
|
|
||||||
|
### Modifié — la carte de l'accueil liste TOUTES les prochaines prises
|
||||||
|
|
||||||
|
- Miroir de l'Android v1.9.6 (remontée : le rappel quotidien CPA masquait
|
||||||
|
le rappel hebdomadaire EEn — « je ne le vois quasiment pas ») : la carte
|
||||||
|
« Prochaine dose » devient « **Prochaines doses** » (titre pluriel dès 2
|
||||||
|
lignes) — **une ligne par traitement, TRIÉES par prochaine prise** ;
|
||||||
|
la 1ʳᵉ ligne (la plus proche) reste mise en avant. Format de ligne
|
||||||
|
inchangé (`Xj Yh · sam. 26 10:00 (nom)`, `XhMM · HH:mm`, `N min`).
|
||||||
|
- Moteur : helper PUR `nextDoseLineText` dans `format.js` (miroir du
|
||||||
|
Kotlin `nextDoseLine`) — 3 tests miroirs dans misc.test.js.
|
||||||
|
- Les NOTIFICATIONS de rappel sont inchangées.
|
||||||
|
|
||||||
## [1.9.5] — 2026-09-20
|
## [1.9.5] — 2026-09-20
|
||||||
|
|
||||||
### Corrigé — revue de code post-v1.9.4 (audit lecture-seule des deux dépôts)
|
### Corrigé — revue de code post-v1.9.4 (audit lecture-seule des deux dépôts)
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
import { computeCurve, autoCalibrated, nextReminderFireFor, Alerts, TConfig, pointHoursBefore, nextBloodDrawRecommendation } from '../pk/index.js';
|
import { computeCurve, autoCalibrated, nextReminderFireFor, Alerts, TConfig, pointHoursBefore, nextBloodDrawRecommendation } from '../pk/index.js';
|
||||||
import { store } from '../data/store.js';
|
import { store } from '../data/store.js';
|
||||||
import { t } from '../util/i18n.js';
|
import { t } from '../util/i18n.js';
|
||||||
import { formatDose, formatValue, formatTime, formatWeekdayTime, formatDateTime, HrtDuration } from '../util/format.js';
|
import { formatDose, formatValue, formatDateTime, formatWeekdayTime, nextDoseLineText } from '../util/format.js';
|
||||||
import { el } from './components.js';
|
import { el } from './components.js';
|
||||||
import { drawChart, COLORS } from './chart-canvas.js';
|
import { drawChart, COLORS } from './chart-canvas.js';
|
||||||
import { openDoseDialog } from './dialogs.js';
|
import { openDoseDialog } from './dialogs.js';
|
||||||
@ -109,7 +109,14 @@ export function renderHome(container, ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Carte « prochaine dose » (grille Posologie via le moteur, fix #52) ───
|
// ── Carte « prochaines doses » (grille Posologie via le moteur, fix #52) ──
|
||||||
|
// v1.9.6 (demande) : la carte liste TOUTES les prochaines prises, UNE
|
||||||
|
// ligne par traitement TRIÉES par prochaine prise (helper pur
|
||||||
|
// nextDoseLineText, miroir du Kotlin nextDoseLine) — l'ancienne carte ne
|
||||||
|
// montrait que la plus proche, et une prise quotidienne (CPA) masquait
|
||||||
|
// totalement le rappel de l'injection hebdomadaire. Le titre passe au
|
||||||
|
// pluriel dès qu'il y a au moins deux lignes ; la 1ʳᵉ ligne (la plus
|
||||||
|
// proche) reste mise en avant (font-weight 600).
|
||||||
const fires = [];
|
const fires = [];
|
||||||
for (const tr of treatments) {
|
for (const tr of treatments) {
|
||||||
const fire = nextReminderFireFor(tr, doseLogs, nowMs);
|
const fire = nextReminderFireFor(tr, doseLogs, nowMs);
|
||||||
@ -117,24 +124,12 @@ export function renderHome(container, ctx) {
|
|||||||
}
|
}
|
||||||
if (fires.length) {
|
if (fires.length) {
|
||||||
fires.sort((a, b) => a[1] - b[1]);
|
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' });
|
const card = el('div', { class: 'card' });
|
||||||
card.appendChild(el('div', { class: 'label' }, t('next_dose')));
|
card.appendChild(el('div', { class: 'label' }, t(fires.length === 1 ? 'next_dose' : 'next_doses')));
|
||||||
if (h >= 24) {
|
fires.forEach(([treatment, next], idx) => {
|
||||||
// v1.4.1 : au-delà de 24 h, afficher en JOURS + jour de semaine
|
card.appendChild(el('div', { style: idx === 0 ? 'font-weight:600;' : '' },
|
||||||
const [days, remH] = HrtDuration.daysAndHours(h);
|
nextDoseLineText(nowMs, next, treatment.name)));
|
||||||
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);
|
col.appendChild(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import { el, showDialog, field, switchRow, filterChip, toast } from './component
|
|||||||
* (APK + web au MÊME numéro, tags appariés `vX.Y.Z` + `vX.Y.Z-web`).
|
* (APK + web au MÊME numéro, tags appariés `vX.Y.Z` + `vX.Y.Z-web`).
|
||||||
* v1.4.10 = portage initial de l'Android v1.4.10.
|
* v1.4.10 = portage initial de l'Android v1.4.10.
|
||||||
*/
|
*/
|
||||||
export const WEB_VERSION = '1.9.5';
|
export const WEB_VERSION = '1.9.6';
|
||||||
|
|
||||||
/** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */
|
/** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */
|
||||||
function parseNumOrNull(text) {
|
function parseNumOrNull(text) {
|
||||||
|
|||||||
@ -113,6 +113,33 @@ export function formatLongDate(ms, timeZone = null) {
|
|||||||
}), timeZone).format(new Date(ms));
|
}), timeZone).format(new Date(ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ligne « prochaine prise » d'un traitement (v1.9.6 — miroir du helper
|
||||||
|
* `nextDoseLine` Kotlin) : « Xj Yh · sam. 26 10:00 (nom) » au-delà de 24 h
|
||||||
|
* (v1.4.1 : les heures deviennent peu lisibles), « XhMM · HH:mm (nom) » en
|
||||||
|
* dessous, « N min · HH:mm (nom) » dans l'heure. PUR, testé dans
|
||||||
|
* tests/misc.test.js — sert à CHAQUE ligne de la carte « Prochaines doses »
|
||||||
|
* de l'accueil (une ligne par traitement, triées par prochaine prise).
|
||||||
|
*
|
||||||
|
* @param {number} nowMs maintenant
|
||||||
|
* @param {number} fireMs prochaine prise du traitement
|
||||||
|
* @param {string} name nom du traitement
|
||||||
|
* @param {string} [timeZone=null]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function nextDoseLineText(nowMs, fireMs, name, timeZone = null) {
|
||||||
|
const deltaMs = fireMs - nowMs;
|
||||||
|
const h = Math.floor(deltaMs / 3600000);
|
||||||
|
const m = Math.floor((deltaMs % 3600000) / 60000);
|
||||||
|
if (h >= 24) {
|
||||||
|
const days = Math.floor(h / 24);
|
||||||
|
const remH = h % 24;
|
||||||
|
return `${days}j ${remH}h · ${formatWeekdayTime(fireMs, timeZone)} (${name})`;
|
||||||
|
}
|
||||||
|
if (h > 0) return `${h}h${String(m).padStart(2, '0')} · ${formatTime(fireMs, timeZone)} (${name})`;
|
||||||
|
return `${m} min · ${formatTime(fireMs, timeZone)} (${name})`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* « EEE d HH:mm » (carte prochaine dose au-delà de 24 h — jour de semaine
|
* « EEE d HH:mm » (carte prochaine dose au-delà de 24 h — jour de semaine
|
||||||
* inclus, cf v1.4.1).
|
* inclus, cf v1.4.1).
|
||||||
|
|||||||
@ -42,6 +42,7 @@ const EN = {
|
|||||||
no_data_short: 'No data — log an injection to see estimates',
|
no_data_short: 'No data — log an injection to see estimates',
|
||||||
delta_6h: '{0} {1} pg/mL vs 6 h ago',
|
delta_6h: '{0} {1} pg/mL vs 6 h ago',
|
||||||
next_dose: 'Next dose',
|
next_dose: 'Next dose',
|
||||||
|
next_doses: 'Upcoming doses',
|
||||||
next_dose_days: '{0} d {1} h · {2} ({3})',
|
next_dose_days: '{0} d {1} h · {2} ({3})',
|
||||||
logged_today: 'Quick log',
|
logged_today: 'Quick log',
|
||||||
home_chart_title: 'Last 24 h',
|
home_chart_title: 'Last 24 h',
|
||||||
@ -252,6 +253,7 @@ const FR = {
|
|||||||
no_data_short: 'Aucune donnée — logue une injection pour voir les estimations',
|
no_data_short: 'Aucune donnée — logue une injection pour voir les estimations',
|
||||||
delta_6h: '{0} {1} pg/mL vs il y a 6 h',
|
delta_6h: '{0} {1} pg/mL vs il y a 6 h',
|
||||||
next_dose: 'Prochaine dose',
|
next_dose: 'Prochaine dose',
|
||||||
|
next_doses: 'Prochaines doses',
|
||||||
next_dose_days: '{0} j {1} h · {2} ({3})',
|
next_dose_days: '{0} j {1} h · {2} ({3})',
|
||||||
logged_today: 'Log rapide',
|
logged_today: 'Log rapide',
|
||||||
home_chart_title: 'Dernières 24 h',
|
home_chart_title: 'Dernières 24 h',
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { nextReminderFireFor, nextReminderFireMs } from '../js/pk/pk-reminders.j
|
|||||||
import { detectExtrema } from '../js/pk/pk-extrema.js';
|
import { detectExtrema } from '../js/pk/pk-extrema.js';
|
||||||
import { LevelPoint } from '../js/pk/pk-engine.js';
|
import { LevelPoint } from '../js/pk/pk-engine.js';
|
||||||
import { isVersionNewer, sectionsSince } from '../js/util/changelog.js';
|
import { isVersionNewer, sectionsSince } from '../js/util/changelog.js';
|
||||||
import { HrtDuration } from '../js/util/format.js';
|
import { HrtDuration, nextDoseLineText } from '../js/util/format.js';
|
||||||
import { makeTreatment, makeDose, DAY_MS, HOUR_MS, assertClose } from './helpers.js';
|
import { makeTreatment, makeDose, DAY_MS, HOUR_MS, assertClose } from './helpers.js';
|
||||||
|
|
||||||
/** Met (h, m) sur le jour calendaire de [ms] (locale — miroir du module testé). */
|
/** Met (h, m) sur le jour calendaire de [ms] (locale — miroir du module testé). */
|
||||||
@ -212,3 +212,30 @@ describe('HrtDuration (temps sous THS)', () => {
|
|||||||
assert.throws(() => assertClose(300, 100, 0.02));
|
assert.throws(() => assertClose(300, 100, 0.02));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('nextDoseLineText (carte « Prochaines doses » — v1.9.6, miroir NextDoseLineTest.kt)', () => {
|
||||||
|
// Hors DST : les heures attendues sont construites avec les MÊMES
|
||||||
|
// primitives (Intl fuseau système) que le module testé.
|
||||||
|
const now = new Date(2026, 8, 21, 12, 0, 0).getTime(); // lundi 21/09/2026 12:00
|
||||||
|
const timeOf = (h, m) => new Date(2026, 8, 21, h, m, 0).getTime();
|
||||||
|
|
||||||
|
test('au-delà de 24 h — jours + jour de semaine', () => {
|
||||||
|
// fire = samedi 26/09 10:00 (118 h plus tard)
|
||||||
|
const fire = now + 118 * HOUR_MS;
|
||||||
|
const line = nextDoseLineText(now, fire, 'Injection EEn 5mg 7d');
|
||||||
|
assert.match(line, /^4j 22h · .+ \(Injection EEn 5mg 7d\)$/);
|
||||||
|
// le jour de semaine est présent (le format Intl diffère selon la locale
|
||||||
|
// système : on épingle la STRUCTURE, pas le libellé)
|
||||||
|
assert.ok(!line.startsWith('118'), 'delta affiché en jours, pas en heures');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('entre 1 h et 24 h — heures + minutes', () => {
|
||||||
|
const fire = timeOf(14, 5);
|
||||||
|
assert.equal(nextDoseLineText(now, fire, 'CPA'), `2h05 · 14:05 (CPA)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('moins d\'une heure — minutes', () => {
|
||||||
|
const fire = timeOf(12, 7);
|
||||||
|
assert.equal(nextDoseLineText(now, fire, 'CPA'), `7 min · 12:07 (CPA)`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user