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.
106 lines
4.5 KiB
JavaScript
106 lines
4.5 KiB
JavaScript
/**
|
||
* ─────────────────────────────────────────────────────────────────────────────
|
||
* Rappels (partie 3/4 du moteur) — portage de `PharmacokineticEngine.kt`
|
||
* sections `nextReminderFireFor` / `nextReminderFireMs` (fix #52, v1.4.0).
|
||
*
|
||
* Le WEB ne peut pas programmer d'alarmes système : ces fonctions alimentent
|
||
* la boucle de vérification de l'app (`ui/reminders.js`, toutes les 30 s
|
||
* tant que la page est ouverte) et la carte « Prochaine dose » de l'accueil.
|
||
* Limite documentée : pas de notification quand l'onglet est fermé.
|
||
*
|
||
* ⚠️ FIX #52 (v1.4.0) : si le traitement a une Posologie
|
||
* (`forecastIntervalDays`) ET un historique de doses, le rappel suit la
|
||
* GRILLE des prises (dernière dose réelle + k × intervalle, créneaux passés
|
||
* sautés — même sémantique que generateForecastDoses) et sonne le jour du
|
||
* créneau à l'heure choisie : une injection tous les samedis à 18 h →
|
||
* rappel UNIQUEMENT le samedi à 18 h. Sinon (gel/oral, ou aucune dose pour
|
||
* cadrer la grille) → comportement QUOTIDIEN historique (prochaine
|
||
* occurrence HH:mm).
|
||
*
|
||
* PUR, testable en Node (web/tests/reminders.test.js).
|
||
* ─────────────────────────────────────────────────────────────────────────────
|
||
*/
|
||
|
||
import { HOUR_MS } from './pk-engine.js';
|
||
|
||
/** Une journée en ms. */
|
||
const DAY_MS = 86400000;
|
||
|
||
/**
|
||
* Met une date sur HH:mm:00.000 du MÊME jour calendaire (heure LOCALE du
|
||
* navigateur / du process Node — équivalent Calendar.set(HOUR_OF_DAY…)).
|
||
*
|
||
* @param {number} ms
|
||
* @param {number} hour
|
||
* @param {number} minute
|
||
* @returns {number} epoch ms
|
||
*/
|
||
function atTimeSameDay(ms, hour, minute) {
|
||
const d = new Date(ms);
|
||
d.setHours(hour, minute, 0, 0);
|
||
return d.getTime();
|
||
}
|
||
|
||
/**
|
||
* Prochain déclenchement de rappel pour UN traitement.
|
||
*
|
||
* @param {object} treatment (reminderEnabled, reminderHour/Minute, isActive,
|
||
* forecastIntervalDays)
|
||
* @param {object[]} doseLogs Toutes les doses (le traitement repère les siennes)
|
||
* @param {number} nowMs
|
||
* @returns {number|null} epoch ms du prochain déclenchement, null si désactivé
|
||
*/
|
||
export function nextReminderFireFor(treatment, doseLogs, nowMs) {
|
||
if (!treatment.reminderEnabled || !treatment.isActive) return null;
|
||
const h = treatment.reminderHour;
|
||
const m = treatment.reminderMinute;
|
||
if (h === null || h === undefined || m === null || m === undefined) return null;
|
||
|
||
const intervalDays = treatment.forecastIntervalDays;
|
||
const myDoses = doseLogs.filter(
|
||
(d) => d.treatmentId === treatment.id && d.timestamp <= nowMs,
|
||
);
|
||
if (intervalDays !== null && intervalDays !== undefined && intervalDays > 0.0 && myDoses.length > 0) {
|
||
const intervalMs = Math.trunc(intervalDays * 24.0 * HOUR_MS);
|
||
if (intervalMs > 0) {
|
||
// Dernière dose réelle = ancre de la grille
|
||
let slot = myDoses.reduce((acc, d) => (acc === null || d.timestamp > acc.timestamp ? d : acc), null).timestamp;
|
||
// Le premier créneau STRICTEMENT futur (un créneau passé = oubli :
|
||
// on avance au rythme configuré — cf generateForecastDoses, #35).
|
||
while (slot <= nowMs) slot += intervalMs;
|
||
// L'alarme sonne le jour du créneau, à l'heure de rappel choisie ;
|
||
// si cette heure est déjà passée le jour du créneau → créneau suivant.
|
||
let candidate = slot;
|
||
for (;;) {
|
||
const fireMs = atTimeSameDay(candidate, h, m);
|
||
if (fireMs > nowMs) return fireMs;
|
||
candidate += intervalMs;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fallback QUOTIDIEN (comportement historique) : prochaine occurrence HH:mm
|
||
let fire = atTimeSameDay(nowMs, h, m);
|
||
if (fire <= nowMs) fire += DAY_MS;
|
||
return fire;
|
||
}
|
||
|
||
/**
|
||
* Prochain rappel parmi tous les traitements (min). [doseLogs] active la
|
||
* planification par Posologie (cf nextReminderFireFor) ; vide = fallback
|
||
* quotidien pur (compatibilité avec les appels historiques).
|
||
*
|
||
* @param {object[]} treatments
|
||
* @param {object[]} [doseLogs=[]]
|
||
* @param {number} [nowMs=Date.now()]
|
||
* @returns {number|null}
|
||
*/
|
||
export function nextReminderFireMs(treatments, doseLogs = [], nowMs = Date.now()) {
|
||
let next = null;
|
||
for (const tr of treatments) {
|
||
const fire = nextReminderFireFor(tr, doseLogs, nowMs);
|
||
if (fire !== null && (next === null || fire < next)) next = fire;
|
||
}
|
||
return next;
|
||
}
|