diff --git a/README.md b/README.md index 9b27669..910c030 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b445d7a..8269cf0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index c18a888..24b40b0 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -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` | diff --git a/js/data/models.js b/js/data/models.js index c1775cf..2b85cf6 100644 --- a/js/data/models.js +++ b/js/data/models.js @@ -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} treatments + * @returns {Array} 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)); +} diff --git a/js/ui/settings.js b/js/ui/settings.js index 8659bd4..85f53b6 100644 --- a/js/ui/settings.js +++ b/js/ui/settings.js @@ -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) { diff --git a/js/ui/treatments.js b/js/ui/treatments.js index 03a5c8d..9b7af7a 100644 --- a/js/ui/treatments.js +++ b/js/ui/treatments.js @@ -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)); diff --git a/js/util/i18n.js b/js/util/i18n.js index 572897c..47b4f03 100644 --- a/js/util/i18n.js +++ b/js/util/i18n.js @@ -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)', diff --git a/tests/treatments-display.test.js b/tests/treatments-display.test.js new file mode 100644 index 0000000..90b7c23 --- /dev/null +++ b/tests/treatments-display.test.js @@ -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'); + }); +});