HormoneTrack-web/js/pk/pk-reminders.js
Siphonight 491efef8eb v1.4.10 — portage initial de l'app Android v1.4.10
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.
2026-09-08 14:34:58 +02:00

106 lines
4.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ─────────────────────────────────────────────────────────────────────────────
* 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;
}