import { MODES, PK_MODELS, CLINICAL_THRESHOLDS, APP_VERSION, generateSimulationData, processTimelineEvents } from './pkCore.js'; import { adjustChartYScale, updateChartAnnotations } from './chartManager.js'; import { initCalculatorUI } from './calculator.js'; import { exportSimulationToCSV } from './csvExport.js'; import { initI18n, getCurrentLang, t } from './i18n.js'; let currentMode = MODES.ESKETAMINE; let pkChart = null; let currentSimulationData = null; const THEME_STORAGE_KEY = 'keta_theme'; const THEMES = ['dark', 'light', 'monokai']; let timerMin = 5; let timespanCount = 60; function buildTimelineInputs(interval, steps) { return Array.from({ length: steps + 1 }, (_, i) => ({ time: i * interval, bolusMg: 0, perfRate: null })); } let timelineInputs = buildTimelineInputs(timerMin, timespanCount); function updateVersionDisplay() { const versionEl = document.querySelector('[data-i18n="app.version"]'); if (versionEl) { versionEl.textContent = t('app.version', { version: APP_VERSION }); } } function updateDynamicLabels() { const total = timerMin * timespanCount; const titleEl = document.querySelector('[data-i18n="timeline.title"]'); if (titleEl) titleEl.textContent = t('timeline.title', { total }); const summaryEl = document.querySelector('[data-i18n="table.summary_title"]'); if (summaryEl) summaryEl.textContent = t('table.summary_title', { total }); const totalEl = document.getElementById('totalTimespanDisplay'); if (totalEl) totalEl.textContent = t('patient.total_label', { total }); } function getStoredTheme() { const stored = localStorage.getItem(THEME_STORAGE_KEY); return THEMES.includes(stored) ? stored : 'dark'; } function applyTheme(theme) { const resolved = THEMES.includes(theme) ? theme : 'dark'; document.documentElement.setAttribute('data-theme', resolved); localStorage.setItem(THEME_STORAGE_KEY, resolved); refreshChartTheme(); } function refreshChartTheme() { if (!pkChart) return; const styles = getComputedStyle(document.documentElement); const gridColor = styles.getPropertyValue('--kt-chart-grid').trim(); const textColor = styles.getPropertyValue('--kt-chart-text').trim(); const legendColor = styles.getPropertyValue('--kt-chart-legend').trim(); pkChart.options.scales.y.title.color = textColor; pkChart.options.scales.y.grid.color = gridColor; pkChart.options.scales.x.title.color = textColor; pkChart.options.scales.x.grid.color = gridColor; pkChart.options.plugins.legend.labels.color = legendColor; // Les couleurs de texte des zones cliniques dépendent aussi du thème (cf. chartManager.js) updateChartAnnotations(pkChart, currentMode, pkChart.options.scales.y.max); pkChart.update(); } function readPositiveInt(el, fallback, max = Infinity) { const parsed = parseInt(el.value.trim(), 10); if (isNaN(parsed) || parsed < 1) return fallback; return Math.min(parsed, max); } function enforceIntegerInput(el) { el?.addEventListener('input', () => { const cleaned = el.value.replace(/[^\d]/g, ''); if (cleaned !== el.value) el.value = cleaned; }); } function rebuildTimeline() { timelineInputs = buildTimelineInputs(timerMin, timespanCount); pkChart.data.labels = timelineInputs.map(d => `${d.time}'`); renderTimelineGrid(); updateDynamicLabels(); updateSimulation(); } document.addEventListener('DOMContentLoaded', async () => { applyTheme(getStoredTheme()); await initI18n(); updateVersionDisplay(); const selectLang = document.getElementById('selectLang'); if (selectLang) { selectLang.value = getCurrentLang(); selectLang.addEventListener('change', async (e) => { await initI18n(e.target.value); updateVersionDisplay(); renderTimelineGrid(); updateDynamicLabels(); updateSimulation(); }); } const selectTheme = document.getElementById('selectTheme'); if (selectTheme) { selectTheme.value = getStoredTheme(); selectTheme.addEventListener('change', (e) => applyTheme(e.target.value)); } const timerInput = document.getElementById('timerInput'); const timespanInput = document.getElementById('timespanInput'); if (timerInput) { timerInput.value = timerMin; enforceIntegerInput(timerInput); timerInput.addEventListener('change', () => { timerMin = readPositiveInt(timerInput, timerMin); timerInput.value = timerMin; rebuildTimeline(); }); } if (timespanInput) { timespanInput.value = timespanCount; enforceIntegerInput(timespanInput); timespanInput.addEventListener('change', () => { timespanCount = readPositiveInt(timespanInput, timespanCount, 500); timespanInput.value = timespanCount; rebuildTimeline(); }); } initChart(); renderTimelineGrid(); updateDynamicLabels(); initCalculatorUI(); setupEventListeners(); updateSimulation(); }); function initChart() { const ctx = document.getElementById('pkChart').getContext('2d'); const styles = getComputedStyle(document.documentElement); const gridColor = styles.getPropertyValue('--kt-chart-grid').trim(); const textColor = styles.getPropertyValue('--kt-chart-text').trim(); const legendColor = styles.getPropertyValue('--kt-chart-legend').trim(); pkChart = new Chart(ctx, { type: 'line', data: { labels: timelineInputs.map(d => `${d.time}'`), datasets: [] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: CLINICAL_THRESHOLDS[currentMode].maxScale, title: { display: true, text: 'Cp (ng/mL)', color: textColor }, grid: { color: gridColor } }, x: { title: { display: true, text: 'Temps (minutes)', color: textColor }, grid: { color: gridColor } } }, plugins: { legend: { labels: { color: legendColor } }, annotation: { annotations: {} } } } }); } function updateSimulation() { const weightInput = document.getElementById('patientWeight'); const weight = parseFloat(weightInput?.value) || 70; const unit = document.getElementById('infusionUnitSelect')?.value || 'mg_kg_h'; // Avertissement de poids (30-120 kg) const weightWarning = document.getElementById('weightWarning'); if (weightWarning) { if (weight < 30 || weight > 120) { weightWarning.classList.remove('hidden'); } else { weightWarning.classList.add('hidden'); } } const datasets = []; const dominoData = document.getElementById('chkDomino')?.checked ? generateSimulationData({ model: PK_MODELS.DOMINO, weightKg: weight, timelineInputs, infusionUnit: unit }) : null; const clementsData = document.getElementById('chkClements')?.checked ? generateSimulationData({ model: PK_MODELS.CLEMENTS, weightKg: weight, timelineInputs, infusionUnit: unit }) : null; const kampData = document.getElementById('chkKamp')?.checked ? generateSimulationData({ model: PK_MODELS.KAMP, weightKg: weight, timelineInputs, infusionUnit: unit }) : null; if (dominoData) { datasets.push({ label: PK_MODELS.DOMINO.name, data: dominoData.map(d => d.cp), borderColor: PK_MODELS.DOMINO.color, borderWidth: 3, tension: 0.2 }); } if (clementsData) { datasets.push({ label: PK_MODELS.CLEMENTS.name, data: clementsData.map(d => d.cp), borderColor: PK_MODELS.CLEMENTS.color, borderWidth: 3, tension: 0.2 }); } if (kampData) { datasets.push({ label: PK_MODELS.KAMP.name, data: kampData.map(d => d.cp), borderColor: PK_MODELS.KAMP.color, borderWidth: 3, tension: 0.2 }); } pkChart.data.datasets = datasets; adjustChartYScale(pkChart, currentMode, datasets); pkChart.update(); currentSimulationData = { timeline: processTimelineEvents(timelineInputs, weight, unit), domino: dominoData, clements: clementsData, kamp: kampData }; updateTimelineGridStyles(); renderDataTable(currentSimulationData); } function renderDataTable(simulationData) { const tbody = document.getElementById('dataTableBody'); if (!tbody || !simulationData) return; tbody.innerHTML = ''; simulationData.timeline.forEach((item, index) => { const row = document.createElement('tr'); row.className = index % 2 === 0 ? 'bg-[var(--kt-row-alt-1)]' : 'bg-[var(--kt-row-alt-2)]'; const dominoCp = (simulationData.domino && simulationData.domino[index]) ? Math.round(simulationData.domino[index].cp) : '-'; const clementsCp = (simulationData.clements && simulationData.clements[index]) ? Math.round(simulationData.clements[index].cp) : '-'; const kampCp = (simulationData.kamp && simulationData.kamp[index]) ? Math.round(simulationData.kamp[index].cp) : '-'; row.innerHTML = ` ${item.time} min ${item.bolusMg ? item.bolusMg : '-'} ${item.infusionRate !== null && item.infusionRate !== undefined ? item.infusionRate : '0'} ${dominoCp} ${clementsCp} ${kampCp} `; tbody.appendChild(row); }); } function renderTimelineGrid() { const container = document.getElementById('timelineGrid'); if (!container) return; container.innerHTML = ''; timelineInputs.forEach((item, idx) => { const col = document.createElement('div'); col.className = 'flex-1 bg-[var(--kt-surface-3)] border border-[var(--kt-border-soft)] rounded-xl p-2 text-center text-xs flex flex-col gap-1.5 min-w-[85px]'; col.innerHTML = `
${item.time} min
${t('timeline.bolus')}
${t('timeline.perf')}
`; container.appendChild(col); }); updateTimelineGridStyles(); } function updateTimelineGridStyles() { const container = document.getElementById('timelineGrid'); if (!container) return; const weight = parseFloat(document.getElementById('patientWeight')?.value) || 70; const unit = document.getElementById('infusionUnitSelect')?.value || 'mg_kg_h'; const processed = processTimelineEvents(timelineInputs, weight, unit); const perfInputs = container.querySelectorAll('input[data-field="perfRate"]'); const bolusInputs = container.querySelectorAll('input[data-field="bolusMg"]'); perfInputs.forEach((input) => { const idx = parseInt(input.dataset.idx, 10); const event = processed[idx]; const isFocused = (document.activeElement === input); if (event.isExplicit) { input.className = 'w-full border text-center rounded py-1 outline-none transition-colors bg-cyan-950/90 border-cyan-400 text-cyan-200 font-bold focus:border-cyan-300 shadow-sm shadow-cyan-950'; if (!isFocused) { input.value = timelineInputs[idx].perfRate !== null ? timelineInputs[idx].perfRate : ''; } } else { input.className = 'w-full border text-center rounded py-1 outline-none transition-colors bg-[var(--kt-surface-3)] border-[var(--kt-border-soft)] text-[var(--kt-text-3)] font-normal italic focus:border-amber-500'; if (!isFocused) { input.value = event.infusionRate > 0 ? event.infusionRate : ''; } } }); bolusInputs.forEach((input) => { const idx = parseInt(input.dataset.idx, 10); const isFocused = (document.activeElement === input); if (!isFocused) { input.value = timelineInputs[idx].bolusMg || ''; } }); } function setupEventListeners() { document.getElementById('timelineGrid')?.addEventListener('input', (e) => { const idx = e.target.dataset.idx; const field = e.target.dataset.field; if (idx !== undefined && field) { const rawVal = e.target.value.trim().replace(',', '.'); if (field === 'perfRate') { if (rawVal === '') { timelineInputs[idx].perfRate = null; } else { const parsed = parseFloat(rawVal); timelineInputs[idx].perfRate = isNaN(parsed) ? null : parsed; } } else { if (rawVal === '') { timelineInputs[idx].bolusMg = 0; } else { const parsed = parseFloat(rawVal); timelineInputs[idx].bolusMg = isNaN(parsed) ? 0 : parsed; } } updateSimulation(); } }); document.getElementById('btnEsk')?.addEventListener('click', () => setMode(MODES.ESKETAMINE)); document.getElementById('btnRac')?.addEventListener('click', () => setMode(MODES.RACEMIQUE)); ['patientName', 'patientWeight', 'infusionUnitSelect', 'chkDomino', 'chkClements', 'chkKamp'].forEach(id => { document.getElementById(id)?.addEventListener('change', updateSimulation); document.getElementById(id)?.addEventListener('input', updateSimulation); }); document.getElementById('btnRaz')?.addEventListener('click', () => { timelineInputs.forEach(item => { item.bolusMg = 0; item.perfRate = null; }); const container = document.getElementById('timelineGrid'); if (container) { container.querySelectorAll('input').forEach(inp => inp.value = ''); } updateSimulation(); }); const triggerExport = () => { if (!currentSimulationData) { updateSimulation(); } const name = document.getElementById('patientName')?.value || ''; const weight = parseFloat(document.getElementById('patientWeight')?.value) || 70; const unit = document.getElementById('infusionUnitSelect')?.value || 'mg_kg_h'; exportSimulationToCSV(currentSimulationData, { name: name, weight: weight, infusionUnit: unit }); }; document.getElementById('btnExportCSVNav')?.addEventListener('click', triggerExport); document.getElementById('btnExportCSV')?.addEventListener('click', triggerExport); } function setMode(mode) { currentMode = mode; const btnEsk = document.getElementById('btnEsk'); const btnRac = document.getElementById('btnRac'); if (mode === MODES.ESKETAMINE) { btnEsk.className = 'px-4 py-1.5 rounded-lg text-xs font-bold bg-amber-500 text-slate-950 shadow-md shadow-amber-500/30'; btnRac.className = 'px-4 py-1.5 rounded-lg text-xs font-bold text-[var(--kt-text-3)] hover:text-[var(--kt-text-1)]'; } else { btnRac.className = 'px-4 py-1.5 rounded-lg text-xs font-bold bg-fuchsia-500 text-slate-950 shadow-md shadow-fuchsia-500/30'; btnEsk.className = 'px-4 py-1.5 rounded-lg text-xs font-bold text-[var(--kt-text-3)] hover:text-[var(--kt-text-1)]'; } updateSimulation(); }