/** * ───────────────────────────────────────────────────────────────────────────── * Dialogs de saisie (portage de `ui/components/DoseDialog.kt` et * `LabDialog.kt`). * * DoseDialog : * - create (traitements ACTIFS proposés) / edit (TOUS les traitements — * la dose éditée reste rattachable à son traitement même inactivé, §6.bis) ; * - override d'ESTER par injection (comme le tableur d'origine) : la liste * suit le MODÈLE du traitement via choicesForModel (v1.9.4 : TFS=7, * WHSAH=6, ESE analytique=6). * * LabDialog (v1.2.2) : prise de sang E2 + T en UNE entrée (chacune * optionnelle) → 1 ou 2 LabResult au même timestamp ; unités T suggérées. * ───────────────────────────────────────────────────────────────────────────── */ import { choicesForModel, esterLabel, isInjectionRoute } from '../data/models.js'; import { store } from '../data/store.js'; import { t } from '../util/i18n.js'; import { formatDose } from '../util/format.js'; import { el, showDialog, field, dateTimeField } from './components.js'; /** * Dialog dose (create ou edit). * * @param {object} opts * treatments : liste proposée (actifs en création, tous en édition) * preselected : Treatment présélectionné (chips accueil / rappel) ou null * existing : DoseLog en édition, ou null (création) */ export function openDoseDialog({ treatments, preselected = null, existing = null }) { const isEdit = !!existing; if (treatments.length === 0) { const dlgEmpty = showDialog({ title: t('add_dose'), body: el('p', {}, t('no_treatment_hint')), actions: [{ label: t('ok'), className: 'btn', onClick: () => dlgEmpty.close() }], }); return; } // État du dialog let selectedId = existing ? existing.treatmentId : (preselected && treatments.some((x) => x.id === preselected.id) ? preselected.id : (treatments[0] ? treatments[0].id : null)); const selected = () => treatments.find((x) => x.id === selectedId) || null; let esterOverride = existing ? existing.esterType : null; // ── construction du corps ──────────────────────────────────────────────── const body = el('div', {}); const treatmentSelect = field({ label: t('treatment_name'), value: selectedId ? String(selectedId) : '', options: treatments.map((tr) => ({ value: String(tr.id), label: `${tr.name} · ${formatDose(tr.doseAmount)} ${tr.doseUnit}` })), onInput: (v) => { selectedId = Number(v); esterOverride = null; // miroir : changer de traitement réinitialise l'override rebuildEster(); amount.input.value = formatDose(selected().doseAmount); }, }); body.appendChild(treatmentSelect); // Override d'ester (seulement si le traitement a un PROFIL PK) const esterWrap = el('div', {}); body.appendChild(esterWrap); function rebuildEster() { esterWrap.textContent = ''; const tr = selected(); if (!tr) return; // Propriété dérivée (miroir treatmentFlags) : injection + ester ≠ NONE const usesProfileModel = isInjectionRoute(tr.route) && tr.esterType !== 'NONE'; if (!usesProfileModel) return; const choices = [ { value: '', label: t('ester_default', tr.esterType) }, ...choicesForModel(tr.pkModel).map((e) => ({ value: e, label: esterLabel(e) })), ]; esterWrap.appendChild(field({ label: t('ester'), value: esterOverride || '', options: choices, onInput: (v) => { esterOverride = v === '' ? null : v; }, })); } const amount = field({ label: `${t('dose_amount')} (${selected() ? selected().doseUnit : ''})`, type: 'number', value: formatDose(existing ? existing.doseAmount : (selected() ? selected().doseAmount : 0)), attrs: { step: 'any', min: '0', inputmode: 'decimal' }, }); body.appendChild(amount); const dt = dateTimeField(t('dose_time'), existing ? existing.timestamp : Date.now(), () => {}); body.appendChild(dt); const notes = field({ label: t('dose_notes'), type: 'textarea', value: existing ? existing.notes || '' : '' }); body.appendChild(notes); rebuildEster(); const dlg = showDialog({ title: isEdit ? t('edit_dose') : t('add_dose'), body, actions: [ { label: t('cancel'), className: 'btn text', onClick: () => dlg.close() }, { label: t('save'), className: 'btn', onClick: () => { const tr = selected(); const value = Number(String(amount.input.value).replace(',', '.')); const ts = dt.getMs(); if (!tr || Number.isNaN(value) || value <= 0 || ts === null) return; // garde de saisie const data = { treatmentId: tr.id, timestamp: ts, doseAmount: value, notes: notes.input.value.trim() === '' ? null : notes.input.value, esterType: esterOverride, }; if (isEdit) { store.upsertDoseLog({ ...existing, ...data }); } else { store.upsertDoseLog(data); } dlg.close(); }, }, ], }); } /** * Dialog lab : prise de sang E2 + T en UNE entrée (chacune optionnelle). * * @param {object} opts { existing: LabResult|null } * - create : deux sections optionnelles → 1 ou 2 LabResult au même timestamp ; * - edit : une seule entrée (le regroupement E2+T reste intact). */ export function openLabDialog({ existing = null }) { const isEdit = !!existing; const body = el('div', {}); const dt = dateTimeField(t('lab_timestamp'), existing ? existing.timestamp : Date.now(), () => {}); body.appendChild(dt); // Suggestions d'unités T (miroir LabDialog Android) : ng/mL, ng/dL, ng/L, // nmol/L — la conversion vers ng/mL se fait à la calibration et au rendu. const UNIT_OPTIONS = ['pg/mL', 'ng/mL', 'ng/dL', 'ng/L', 'nmol/L', 'mIU/L']; let e2Input = null; let e2Notes = null; let tInput = null; let tUnit = null; if (isEdit) { const isT = String(existing.marker).toUpperCase() === 'T'; const valueField = field({ label: isT ? t('lab_t_value') : t('lab_e2_value'), type: 'number', value: String(existing.value), attrs: { step: 'any', inputmode: 'decimal' }, }); body.appendChild(valueField); if (isT) { tInput = valueField.input; const unitSel = field({ label: t('dose_unit'), value: existing.unit, options: UNIT_OPTIONS.map((u) => ({ value: u, label: u })), }); body.appendChild(unitSel); tUnit = unitSel.input; } else { e2Input = valueField.input; } } else { // Section E2 (optionnelle) — unité fixée pg/mL (marker E2 = estradiol) const e2Field = field({ label: `${t('lab_e2_value')} (${t('lab_optional')})`, type: 'number', attrs: { step: 'any', inputmode: 'decimal' } }); body.appendChild(e2Field); e2Input = e2Field.input; // Section T (optionnelle) avec choix d'unité const tField = field({ label: `${t('lab_t_value')} (${t('lab_optional')})`, type: 'number', attrs: { step: 'any', inputmode: 'decimal' } }); body.appendChild(tField); tInput = tField.input; const unitSel = field({ label: `${t('dose_unit')} — ${t('lab_units_hint')}`, value: 'ng/mL', options: UNIT_OPTIONS.map((u) => ({ value: u, label: u })), }); body.appendChild(unitSel); tUnit = unitSel.input; } const notes = field({ label: t('lab_notes'), type: 'textarea', value: existing ? existing.notes || '' : '' }); body.appendChild(notes); const dlg = showDialog({ title: isEdit ? t('edit_which_lab') : t('add_lab'), body, actions: [ { label: t('cancel'), className: 'btn text', onClick: () => dlg.close() }, { label: t('save'), className: 'btn', onClick: () => { const ts = dt.getMs(); if (ts === null) return; const notesVal = notes.input.value.trim() === '' ? null : notes.input.value; if (isEdit) { const value = Number(String((tInput || e2Input).value).replace(',', '.')); if (Number.isNaN(value)) return; store.upsertLabResult({ ...existing, value, unit: tInput ? tUnit.value : existing.unit, timestamp: ts, notes: notesVal, }); } else { const e2Val = String(e2Input.value).trim() === '' ? null : Number(e2Input.value.replace(',', '.')); const tVal = String(tInput.value).trim() === '' ? null : Number(tInput.value.replace(',', '.')); if (e2Val === null && tVal === null) return; // au moins une valeur if (e2Val !== null && !Number.isNaN(e2Val)) { store.upsertLabResult({ marker: 'E2', value: e2Val, unit: 'pg/mL', timestamp: ts, notes: notesVal }); } if (tVal !== null && !Number.isNaN(tVal)) { store.upsertLabResult({ marker: 'T', value: tVal, unit: tUnit.value, timestamp: ts, notes: notesVal }); } } dlg.close(); }, }, ], }); }