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:
parent
f36e25ef62
commit
0dd5fa7dfd
13
README.md
13
README.md
@ -17,15 +17,14 @@
|
|||||||
> 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.10.0 — parité fonctionnelle avec l'Android v1.10.0
|
- **Statut** : web v1.11.0 — parité fonctionnelle avec l'Android v1.11.0
|
||||||
sur la logique miroirée (fix #68 : creux PRÉ-INJECTION dans la
|
sur la logique miroirée (page Traitements : inactifs regroupés tout en
|
||||||
recommandation de prise de sang — date toujours postérieure à la
|
bas sous un en-tête dédié, cartes atténuées — helper pur
|
||||||
stabilisation, cohérente avec le créneau affiché ; les autres changements
|
`treatmentsForDisplay` miroir du Kotlin ; le fix #68 de la reco de prise
|
||||||
Android de la v1.10.0 — dialog fantôme #69, rappel sauté si dose déjà
|
de sang reste couvert, cf changelog) ;
|
||||||
loggée — sont sans équivalent web, notifications = Android) ;
|
|
||||||
l'auto-backup journalier Android reste structurellement non porté,
|
l'auto-backup journalier Android reste structurellement non porté,
|
||||||
cf §12 — l'export manuel couvre la donnée ·
|
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 ✅
|
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,22 @@
|
|||||||
> 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.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
|
## [1.10.0] — 2026-09-27
|
||||||
|
|
||||||
### Corrigé — la recommandation de prise de sang précédait la stabilisation (#68)
|
### Corrigé — la recommandation de prise de sang précédait la stabilisation (#68)
|
||||||
|
|||||||
@ -60,7 +60,7 @@ Règles du portage, fixées au départ :
|
|||||||
|
|
||||||
| Choix | Décision | Pourquoi |
|
| 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 |
|
| 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 |
|
| 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())`) |
|
| 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)
|
│ │ créneau, saut de stabilisation, gardes)
|
||||||
│ ├── lab-notes.test.js ← labNotesForDisplay (v1.7.1 : notes d'une
|
│ ├── lab-notes.test.js ← labNotesForDisplay (v1.7.1 : notes d'une
|
||||||
│ │ prise E2+T — les DEUX distinctes affichées)
|
│ │ 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
|
│ ├── lab-trajectory-model.test.js ← LabTrajectoryModelTest.kt (v1.5.0
|
||||||
│ │ « Tracé labs » + v1.6.0 prolongation —
|
│ │ « Tracé labs » + v1.6.0 prolongation —
|
||||||
│ │ passage exact, garde #61, horizon cutoff)
|
│ │ 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` |
|
| 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) |
|
| 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` |
|
| 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` |
|
| Smoke HTTP | curl | ressources clés en 200 | `check.sh --with-serve` |
|
||||||
|
|
||||||
|
|||||||
@ -211,3 +211,21 @@ export function newLabResult(init = {}) {
|
|||||||
...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));
|
||||||
|
}
|
||||||
|
|||||||
@ -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.10.0';
|
export const WEB_VERSION = '1.11.0';
|
||||||
|
|
||||||
/** 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) {
|
||||||
|
|||||||
@ -5,11 +5,15 @@
|
|||||||
* Cartes (nom, voie, dose, chips ester·modèle / Tmax / ×scale / ⏰ rappel,
|
* Cartes (nom, voie, dose, chips ester·modèle / Tmax / ×scale / ⏰ rappel,
|
||||||
* badge inactif) → tap = éditeur. ⚠️ Le chip modèle passe par modelLabelKey
|
* badge inactif) → tap = éditeur. ⚠️ Le chip modèle passe par modelLabelKey
|
||||||
* (fix #59 : un ternaire à 2 branches affichait « Estrannaise » pour WHSAH).
|
* (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 { 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 { t } from '../util/i18n.js';
|
||||||
import { formatDose } from '../util/format.js';
|
import { formatDose } from '../util/format.js';
|
||||||
import { el, assistChip, fab } from './components.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 }
|
* @param {object} ctx { openEditor: (id:number)=>void, openNew: ()=>void }
|
||||||
*/
|
*/
|
||||||
export function renderTreatments(container, ctx) {
|
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', {});
|
const col = el('div', {});
|
||||||
container.appendChild(col);
|
container.appendChild(col);
|
||||||
@ -29,9 +36,18 @@ export function renderTreatments(container, ctx) {
|
|||||||
col.appendChild(el('p', {}, t('no_data')));
|
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 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' });
|
const titleRow = el('div', { class: 'row' });
|
||||||
titleRow.appendChild(el('span', { style: 'font-weight:600;flex:1;' }, tr.name));
|
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}`));
|
chipRow.appendChild(assistChip(`⏰ ${hh}:${mm}`));
|
||||||
}
|
}
|
||||||
card.appendChild(chipRow);
|
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));
|
container.appendChild(fab(ctx.openNew));
|
||||||
|
|||||||
@ -66,6 +66,7 @@ const EN = {
|
|||||||
confirm_delete_blood_draw: 'Delete the whole blood draw ({0})?',
|
confirm_delete_blood_draw: 'Delete the whole blood draw ({0})?',
|
||||||
// Traitements
|
// Traitements
|
||||||
inactive: 'inactive',
|
inactive: 'inactive',
|
||||||
|
inactive_section: 'Inactive treatments',
|
||||||
pk_absorption: 'Tmax',
|
pk_absorption: 'Tmax',
|
||||||
pk_halflife: 'Half-life (h)',
|
pk_halflife: 'Half-life (h)',
|
||||||
pk_bioavail: 'Bioavailability (0–1)',
|
pk_bioavail: 'Bioavailability (0–1)',
|
||||||
@ -273,6 +274,7 @@ const FR = {
|
|||||||
edit_which_lab: 'Quelle entrée modifier ?',
|
edit_which_lab: 'Quelle entrée modifier ?',
|
||||||
confirm_delete_blood_draw: 'Supprimer la prise de sang entière ({0}) ?',
|
confirm_delete_blood_draw: 'Supprimer la prise de sang entière ({0}) ?',
|
||||||
inactive: 'inactive',
|
inactive: 'inactive',
|
||||||
|
inactive_section: 'Traitements inactifs',
|
||||||
pk_absorption: 'Tmax',
|
pk_absorption: 'Tmax',
|
||||||
pk_halflife: 'Demi-vie (h)',
|
pk_halflife: 'Demi-vie (h)',
|
||||||
pk_bioavail: 'Biodisponibilité (0–1)',
|
pk_bioavail: 'Biodisponibilité (0–1)',
|
||||||
|
|||||||
66
tests/treatments-display.test.js
Normal file
66
tests/treatments-display.test.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user