v1.11.0 (web) : inactifs regroupés en bas de la page Traitements + cartes atténuées — miroir Android

- Helper pur treatmentsForDisplay dans js/data/models.js (miroir du Kotlin,
  tri STABLE : actifs d'abord, inactifs après, ordre relatif conservé dans
  chaque groupe) — demande : « peu discernables, à placer à la fin ».
- Rendu : en-tête « Traitements inactifs » (seulement s'il en existe),
  cartes opacity 0.55 en plus du badge — lisibles et cliquables.
- Clés i18n inactive_section FR/EN ; WEB_VERSION alignée sur 1.11.0.
- 4 tests treatments-display.test.js (miroir TreatmentsDisplayTest.kt).

184 tests + E2E verts (check.sh).
This commit is contained in:
Siphonight 2026-09-29 07:30:59 +02:00
parent f36e25ef62
commit d4c6d2e6ec
8 changed files with 147 additions and 15 deletions

View File

@ -17,15 +17,14 @@
> Fie-toi toujours à tes analyses de sang et aux consignes de ton
> endocrinologue.
- **Statut** : web v1.10.0 — parité fonctionnelle avec l'Android v1.10.0
sur la logique miroirée (fix #68 : creux PRÉ-INJECTION dans la
recommandation de prise de sang — date toujours postérieure à la
stabilisation, cohérente avec le créneau affiché ; les autres changements
Android de la v1.10.0 — dialog fantôme #69, rappel sauté si dose déjà
loggée — sont sans équivalent web, notifications = Android) ;
- **Statut** : web v1.11.0 — parité fonctionnelle avec l'Android v1.11.0
sur la logique miroirée (page Traitements : inactifs regroupés tout en
bas sous un en-tête dédié, cartes atténuées — helper pur
`treatmentsForDisplay` miroir du Kotlin ; le fix #68 de la reco de prise
de sang reste couvert, cf changelog) ;
l'auto-backup journalier Android reste structurellement non porté,
cf §12 — l'export manuel couvre la donnée ·
**180 tests verts** · E2E navigateur ✅ · conteneur testé en
**184 tests verts** · E2E navigateur ✅ · conteneur testé en
configuration durcie ✅ · lint/i18n ✅
- **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)

View File

@ -5,6 +5,22 @@
> section la plus récente EN PREMIER ; ce fichier est lu par le dialog
> « Nouveautés » de l'app web (fetch `docs/CHANGELOG.md`).
## [1.11.0] — 2026-09-29
### Ajouté — les traitements inactifs regroupés en bas de la page Traitements
- Miroir de l'Android v1.11.0 (demande : « les traitements inactifs sont
peu discernables — les placer à la fin, et peut-être les rendre plus
distincts ») : les ACTIFS d'abord, les INACTIFS regroupés **tout en bas**
sous un en-tête « Traitements inactifs » (seulement s'il en existe),
cartes **atténuées** (opacity 0,55) en plus du badge.
- Tri STABLE : l'ordre relatif à l'intérieur de chaque groupe est conservé.
Helper pur `treatmentsForDisplay` dans `js/data/models.js` (miroir du
Kotlin, node-safe) — 4 tests (`tests/treatments-display.test.js`,
miroir de TreatmentsDisplayTest.kt).
Vérifié : 184 tests verts + E2E (`check.sh`).
## [1.10.0] — 2026-09-27
### Corrigé — la recommandation de prise de sang précédait la stabilisation (#68)

View File

@ -60,7 +60,7 @@ Règles du portage, fixées au départ :
| Choix | Décision | Pourquoi |
|---|---|---|
| Langage | JavaScript ES modules natifs (pas de TS, pas de build) | zéro chaîne de compilation → le déploiement = copier le dossier ; les types critiques sont documentés en JSDoc et épinglés par 180 tests |
| Langage | JavaScript ES modules natifs (pas de TS, pas de build) | zéro chaîne de compilation → le déploiement = copier le dossier ; les types critiques sont documentés en JSDoc et épinglés par 184 tests |
| Framework UI | Aucun — DOM via un helper `el()` (`ui/components.js`) | même philosophie que la v1 Android (pas de ViewModel/DI) ; une seule abstraction à connaître |
| Rendu graphique | Canvas 2D natif (portage de `CurveChart.kt`) | pas de lib de charts : fidélité du rendu, zéro dépendance |
| Persistance | `localStorage` (clés préfixées `hormonetrack.`) | données petites (JSON) ; backend INJECTABLE → testable en Node (`store.setBackend(new Map())`) |
@ -260,6 +260,9 @@ HormoneTrack-web/ (dépôt séparé)
│ │ créneau, saut de stabilisation, gardes)
│ ├── lab-notes.test.js ← labNotesForDisplay (v1.7.1 : notes d'une
│ │ prise E2+T — les DEUX distinctes affichées)
│ ├── treatments-display.test.js ← treatmentsForDisplay (v1.11.0 : actifs
│ │ d'abord, inactifs regroupés en bas — tri
│ │ stable)
│ ├── lab-trajectory-model.test.js ← LabTrajectoryModelTest.kt (v1.5.0
│ │ « Tracé labs » + v1.6.0 prolongation —
│ │ passage exact, garde #61, horizon cutoff)
@ -405,7 +408,7 @@ Pièges rencontrés :
|---|---|---|---|
| Syntaxe | `node --check` | tous les modules ES | `bash scripts/check.sh` |
| i18n | `scripts/i18n-check.mjs` | clés FR/EN synchronisées + clés utilisées | (dans check.sh) |
| Unitaires | `node --test` | **180 tests** : noyau PK (ESE analytique/TFS/WHS), calibration, backup, store, helpers, alertes, rappels, changelog, tracé labs, estrannaise-models/cloud, rounding, presets (v1.9.4) | `npm test` |
| Unitaires | `node --test` | **184 tests** : noyau PK (ESE analytique/TFS/WHS), calibration, backup, store, helpers, alertes, rappels, changelog, tracé labs, estrannaise-models/cloud, rounding, presets (v1.9.4), ordre d'affichage des traitements (v1.11.0) | `npm test` |
| E2E navigateur | playwright-core + build Firefox | app réelle : rendu, canvas peint (pixels), navigation, dialog changelog, langue | `npm run e2e` |
| Smoke HTTP | curl | ressources clés en 200 | `check.sh --with-serve` |

View File

@ -211,3 +211,21 @@ export function newLabResult(init = {}) {
...init,
};
}
/**
* ORDRE D'AFFICHAGE de la page Traitements (v1.11.0, miroir fidèle de
* `ui/screens/TreatmentsScreen.kt` — même KDoc) : les ACTIFS d'abord, les
* INACTIFS regroupés TOUT EN BAS — l'ancien ordre brut du store mélangeait
* les deux groupes et le badge discret ne suffisait plus à les discerner
* (« peu discernables », remontée).
*
* Tri STABLE : l'ordre relatif À L'INTÉRIEUR de chaque groupe est conservé.
* PUR (aucune dépendance DOM) → testable en node
* (tests/treatments-display.test.js, miroir de TreatmentsDisplayTest.kt).
*
* @param {Array<object>} treatments
* @returns {Array<object>} nouvelle liste triée (l'entrée n'est pas mutée)
*/
export function treatmentsForDisplay(treatments) {
return [...treatments].sort((a, b) => Number(b.isActive) - Number(a.isActive));
}

View File

@ -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`).
* v1.4.10 = portage initial de l'Android v1.4.10.
*/
export const WEB_VERSION = '1.10.0';
export const WEB_VERSION = '1.11.0';
/** Parse un champ numérique FR (virgule → point) ; null si vide/invalide. */
function parseNumOrNull(text) {

View File

@ -5,11 +5,15 @@
* Cartes (nom, voie, dose, chips ester·modèle / Tmax / ×scale / ⏰ rappel,
* badge inactif) → tap = éditeur. ⚠️ Le chip modèle passe par modelLabelKey
* (fix #59 : un ternaire à 2 branches affichait « Estrannaise » pour WHSAH).
*
* v1.11.0 (demande utilisatrice, miroir Android) : les ACTIFS d'abord, les
* INACTIFS regroupés TOUT EN BAS sous un en-tête de section, cartes
* ATTÉNUÉES (opacity 0.55) en plus du badge — « peu discernables ».
* ─────────────────────────────────────────────────────────────────────────────
*/
import { store } from '../data/store.js';
import { modelLabelKey, routeLabelKey, treatmentFlags } from '../data/models.js';
import { modelLabelKey, routeLabelKey, treatmentFlags, treatmentsForDisplay } from '../data/models.js';
import { t } from '../util/i18n.js';
import { formatDose } from '../util/format.js';
import { el, assistChip, fab } from './components.js';
@ -20,7 +24,10 @@ import { el, assistChip, fab } from './components.js';
* @param {object} ctx { openEditor: (id:number)=>void, openNew: ()=>void }
*/
export function renderTreatments(container, ctx) {
const treatments = store.getTreatments();
// v1.11.0 : actifs d'abord, inactifs regroupés en bas (cf treatmentsForDisplay)
const treatments = treatmentsForDisplay(store.getTreatments());
const active = treatments.filter((tr) => tr.isActive);
const inactive = treatments.filter((tr) => !tr.isActive);
const col = el('div', {});
container.appendChild(col);
@ -29,9 +36,18 @@ export function renderTreatments(container, ctx) {
col.appendChild(el('p', {}, t('no_data')));
}
for (const tr of treatments) {
/**
* Rend UNE carte traitement. @param {object} tr @param {boolean} dimmed
* carte inactive → opacité réduite (second signal visuel au-delà du
* badge ; reste lisible et cliquable).
*/
const renderCard = (tr, dimmed) => {
const flags = treatmentFlags(tr); // miroir des getters Kotlin
const card = el('div', { class: 'card', style: 'cursor:pointer;', onclick: () => ctx.openEditor(tr.id) });
const card = el('div', {
class: 'card',
style: `cursor:pointer;${dimmed ? 'opacity:0.55;' : ''}`,
onclick: () => ctx.openEditor(tr.id),
});
const titleRow = el('div', { class: 'row' });
titleRow.appendChild(el('span', { style: 'font-weight:600;flex:1;' }, tr.name));
@ -55,7 +71,19 @@ export function renderTreatments(container, ctx) {
chipRow.appendChild(assistChip(`⏰ ${hh}:${mm}`));
}
card.appendChild(chipRow);
col.appendChild(card);
return card;
};
for (const tr of active) {
col.appendChild(renderCard(tr, false));
}
// En-tête de section SEULEMENT s'il existe des inactifs (miroir Android)
if (inactive.length > 0) {
col.appendChild(el('p', { class: 'sub', style: 'margin:14px 0 4px;' }, t('inactive_section')));
}
for (const tr of inactive) {
col.appendChild(renderCard(tr, true));
}
container.appendChild(fab(ctx.openNew));

View File

@ -66,6 +66,7 @@ const EN = {
confirm_delete_blood_draw: 'Delete the whole blood draw ({0})?',
// Traitements
inactive: 'inactive',
inactive_section: 'Inactive treatments',
pk_absorption: 'Tmax',
pk_halflife: 'Half-life (h)',
pk_bioavail: 'Bioavailability (0–1)',
@ -273,6 +274,7 @@ const FR = {
edit_which_lab: 'Quelle entrée modifier ?',
confirm_delete_blood_draw: 'Supprimer la prise de sang entière ({0}) ?',
inactive: 'inactive',
inactive_section: 'Traitements inactifs',
pk_absorption: 'Tmax',
pk_halflife: 'Demi-vie (h)',
pk_bioavail: 'Biodisponibilité (0–1)',

View File

@ -0,0 +1,66 @@
/**
* Tests de l'ORDRE D'AFFICHAGE de la page Traitements (v1.11.0, miroir de
* TreatmentsDisplayTest.kt — demande utilisatrice : « les traitements
* inactifs faudrait les placer à la fin (tout en bas) de la liste ») :
* - les ACTIFS d'abord, les INACTIFS regroupés après ;
* - tri STABLE (ordre conservé à l'intérieur de chaque groupe) ;
* - cas limites : liste vide, tout actif, tout inactif (identité).
*/
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { treatmentsForDisplay } from '../js/data/models.js';
import { makeTreatment } from './helpers.js';
describe('treatments display order (v1.11.0)', () => {
test('les inactifs sont regroupés à la fin', () => {
const mixed = [
makeTreatment({ id: 1, name: 'EEn', isActive: true }),
makeTreatment({ id: 2, name: 'EV-old', isActive: false }),
makeTreatment({ id: 3, name: 'CPA', isActive: true }),
makeTreatment({ id: 4, name: 'EEn-old', isActive: false }),
makeTreatment({ id: 5, name: 'Ritaline', isActive: true }),
];
assert.deepEqual(
treatmentsForDisplay(mixed).map((t) => t.id),
[1, 3, 5, 2, 4],
'actifs d\'abord (ordre conservé), inactifs après (ordre conservé)',
);
});
test('tri stable : ordre conservé à l\'intérieur de chaque groupe', () => {
const mixed = [
makeTreatment({ id: 10, name: 'A', isActive: false }),
makeTreatment({ id: 11, name: 'B', isActive: true }),
makeTreatment({ id: 12, name: 'C', isActive: false }),
makeTreatment({ id: 13, name: 'D', isActive: true }),
];
assert.deepEqual(
treatmentsForDisplay(mixed).map((t) => t.id),
[11, 13, 10, 12],
);
});
test('tout actif / tout inactif : ordre inchangé (identité)', () => {
const actifs = [
makeTreatment({ id: 3, isActive: true }),
makeTreatment({ id: 1, isActive: true }),
];
assert.deepEqual(treatmentsForDisplay(actifs).map((t) => t.id), [3, 1]);
const inactifs = [
makeTreatment({ id: 2, isActive: false }),
makeTreatment({ id: 1, isActive: false }),
];
assert.deepEqual(treatmentsForDisplay(inactifs).map((t) => t.id), [2, 1]);
});
test('liste vide reste vide ; entrée non mutée', () => {
assert.equal(treatmentsForDisplay([]).length, 0);
const mixed = [
makeTreatment({ id: 2, isActive: false }),
makeTreatment({ id: 1, isActive: true }),
];
treatmentsForDisplay(mixed);
assert.deepEqual(mixed.map((t) => t.id), [2, 1], 'la liste d\'entrée n\'est pas mutée');
});
});