refacto: Exporting is now in CSV the values + auto gestion of perfusion

This commit is contained in:
gauvainboiche
2026-08-02 14:44:03 +02:00
parent c0843ed083
commit abe44eca1d
9 changed files with 646 additions and 217 deletions
+164 -33
View File
@@ -1,16 +1,17 @@
import { MODES, PK_MODELS, CLINICAL_THRESHOLDS, generateSimulationData } from './pkCore.js';
import { MODES, PK_MODELS, CLINICAL_THRESHOLDS, generateSimulationData, processTimelineEvents } from './pkCore.js';
import { adjustChartYScale } from './chartManager.js';
import { initCalculatorUI } from './calculator.js';
import { generatePDFReport } from './pdfExport.js';
import { exportSimulationToCSV } from './csvExport.js';
import { initI18n, getCurrentLang, t } from './i18n.js';
let currentMode = MODES.ESKETAMINE;
let pkChart = null;
let currentSimulationData = null;
const timelineInputs = Array.from({ length: 61 }, (_, i) => ({
time: i * 5,
bolusMg: 0,
perfRateMgKgH: 0
perfRate: null
}));
document.addEventListener('DOMContentLoaded', async () => {
@@ -66,33 +67,57 @@ function initChart() {
}
function updateSimulation() {
const weight = parseFloat(document.getElementById('inputWeight').value) || 70;
const datasets = [];
const weightInput = document.getElementById('patientWeight');
const weight = parseFloat(weightInput?.value) || 70;
const unit = document.getElementById('infusionUnitSelect')?.value || 'mg_kg_h';
if (document.getElementById('chkDomino').checked) {
// 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: generateSimulationData({ model: PK_MODELS.DOMINO, weightKg: weight, timelineInputs }),
data: dominoData.map(d => d.cp),
borderColor: PK_MODELS.DOMINO.color,
borderWidth: 3,
tension: 0.2
});
}
if (document.getElementById('chkClements').checked) {
if (clementsData) {
datasets.push({
label: PK_MODELS.CLEMENTS.name,
data: generateSimulationData({ model: PK_MODELS.CLEMENTS, weightKg: weight, timelineInputs }),
data: clementsData.map(d => d.cp),
borderColor: PK_MODELS.CLEMENTS.color,
borderWidth: 3,
tension: 0.2
});
}
if (document.getElementById('chkKamp').checked) {
if (kampData) {
datasets.push({
label: PK_MODELS.KAMP.name,
data: generateSimulationData({ model: PK_MODELS.KAMP, weightKg: weight, timelineInputs }),
data: kampData.map(d => d.cp),
borderColor: PK_MODELS.KAMP.color,
borderWidth: 3,
tension: 0.2
@@ -102,77 +127,183 @@ function updateSimulation() {
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-slate-900/30' : 'bg-slate-800/30';
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 = `
<td class="px-3 py-1.5 font-bold">${item.time} min</td>
<td class="px-3 py-1.5">${item.bolusMg ? item.bolusMg : '-'}</td>
<td class="px-3 py-1.5">${item.infusionRate !== null && item.infusionRate !== undefined ? item.infusionRate : '0'}</td>
<td class="px-3 py-1.5 text-cyan-300 font-bold">${dominoCp}</td>
<td class="px-3 py-1.5 text-emerald-300 font-bold">${clementsCp}</td>
<td class="px-3 py-1.5 text-purple-300 font-bold">${kampCp}</td>
`;
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-slate-900 border border-slate-700/60 rounded-xl p-2 text-center text-xs flex flex-col gap-1.5 min-w-[85px]';
col.innerHTML = `
<div class="font-bold text-amber-400 border-b border-slate-800 pb-1">${item.time} min</div>
<div>
<span class="text-[10px] text-slate-400 block mb-0.5">${t('timeline.bolus')}</span>
<input type="number" data-idx="${idx}" data-field="bolusMg" value="${item.bolusMg || ''}" placeholder="0" class="w-full bg-slate-800 border border-slate-700 text-center rounded text-white py-1 outline-none focus:border-amber-500">
<input type="number" step="any" data-idx="${idx}" data-field="bolusMg" value="" placeholder="0" class="w-full bg-slate-800 border border-slate-700 text-center rounded text-white py-1 outline-none focus:border-amber-500">
</div>
<div>
<span class="text-[10px] text-slate-400 block mb-0.5">${t('timeline.perf')}</span>
<input type="number" data-idx="${idx}" data-field="perfRateMgKgH" value="${item.perfRateMgKgH || ''}" placeholder="0" class="w-full bg-slate-800 border border-slate-700 text-center rounded text-white py-1 outline-none focus:border-amber-500">
<input type="number" step="any" data-idx="${idx}" data-field="perfRate" value="" placeholder="0" class="w-full border text-center rounded py-1 outline-none transition-colors">
</div>
`;
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-slate-950/80 border-slate-800/50 text-slate-500 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) => {
document.getElementById('timelineGrid')?.addEventListener('input', (e) => {
const idx = e.target.dataset.idx;
const field = e.target.dataset.field;
if (idx !== undefined && field) {
timelineInputs[idx][field] = parseFloat(e.target.value) || 0;
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));
document.getElementById('btnEsk')?.addEventListener('click', () => setMode(MODES.ESKETAMINE));
document.getElementById('btnRac')?.addEventListener('click', () => setMode(MODES.RACEMIQUE));
['inputWeight', 'chkDomino', 'chkClements', 'chkKamp'].forEach(id => {
document.getElementById(id).addEventListener('change', updateSimulation);
['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.perfRateMgKgH = 0; });
renderTimelineGrid();
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();
});
document.getElementById('btnExportPdf').addEventListener('click', () => {
const patientData = {
weight: document.getElementById('inputWeight').value || 70,
age: document.getElementById('inputAge').value || 50,
height: document.getElementById('inputHeight').value || 170
};
generatePDFReport(pkChart, patientData, currentMode);
});
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');
const concInput = document.getElementById('inputConc');
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-slate-400 hover:text-slate-200';
concInput.value = CLINICAL_THRESHOLDS[MODES.ESKETAMINE].defaultConcentration;
} 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-slate-400 hover:text-slate-200';
concInput.value = CLINICAL_THRESHOLDS[MODES.RACEMIQUE].defaultConcentration;
}
updateSimulation();
}