mirror of
https://github.com/gauvainboiche/ketapk.git
synced 2026-09-02 11:13:11 +02:00
refacto: Exporting is now in CSV the values + auto gestion of perfusion
This commit is contained in:
+164
-33
@@ -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();
|
||||
}
|
||||
@@ -8,11 +8,10 @@ export function initCalculatorUI() {
|
||||
if (!modal || !btnOpen || !btnClose) return;
|
||||
|
||||
btnOpen.addEventListener('click', () => {
|
||||
const mainWeight = document.getElementById('inputWeight')?.value || 70;
|
||||
const mainConc = document.getElementById('inputConc')?.value || 5;
|
||||
|
||||
document.getElementById('calcWeight').value = mainWeight;
|
||||
document.getElementById('calcConc').value = mainConc;
|
||||
// Utilise patientWeight à la place d'inputWeight
|
||||
const mainWeight = document.getElementById('patientWeight')?.value || 70;
|
||||
const calcWeightInput = document.getElementById('calcWeight');
|
||||
if (calcWeightInput) calcWeightInput.value = mainWeight;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
});
|
||||
@@ -35,22 +34,22 @@ export function initCalculatorUI() {
|
||||
|
||||
const getWeight = () => parseFloat(calcWeightInput.value) || 70;
|
||||
|
||||
mgInput.addEventListener('input', () => {
|
||||
mgInput?.addEventListener('input', () => {
|
||||
const mg = parseFloat(mgInput.value);
|
||||
mgKgInput.value = isNaN(mg) ? '' : (mg / getWeight()).toFixed(2);
|
||||
});
|
||||
|
||||
mgKgInput.addEventListener('input', () => {
|
||||
mgKgInput?.addEventListener('input', () => {
|
||||
const mgKg = parseFloat(mgKgInput.value);
|
||||
mgInput.value = isNaN(mgKg) ? '' : (mgKg * getWeight()).toFixed(1);
|
||||
});
|
||||
|
||||
mgKgHInput.addEventListener('input', () => {
|
||||
mgKgHInput?.addEventListener('input', () => {
|
||||
const mgKgH = parseFloat(mgKgHInput.value);
|
||||
ugKgMinInput.value = isNaN(mgKgH) ? '' : CalculatorEngine.mgKgHToMicrogKgMin(mgKgH).toFixed(1);
|
||||
});
|
||||
|
||||
ugKgMinInput.addEventListener('input', () => {
|
||||
ugKgMinInput?.addEventListener('input', () => {
|
||||
const ug = parseFloat(ugKgMinInput.value);
|
||||
mgKgHInput.value = isNaN(ug) ? '' : CalculatorEngine.microgKgMinToMgKgH(ug).toFixed(2);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { t } from './i18n.js';
|
||||
|
||||
/**
|
||||
* Formate la date au format strict : YYYY_DD_MM_HH_MM_SS
|
||||
*/
|
||||
function getFormattedTimestamp() {
|
||||
const now = new Date();
|
||||
const YYYY = now.getFullYear();
|
||||
const DD = String(now.getDate()).padStart(2, '0');
|
||||
const MM = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const HH = String(now.getHours()).padStart(2, '0');
|
||||
const Min = String(now.getMinutes()).padStart(2, '0');
|
||||
const SS = String(now.getSeconds()).padStart(2, '0');
|
||||
return `${YYYY}_${DD}_${MM}_${HH}_${Min}_${SS}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforme le nom saisi en slug propre (ex: "DUPONT Jean" -> "dupont_jean")
|
||||
*/
|
||||
function slugify(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.toString()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // Enlève les accents
|
||||
.replace(/[^a-z0-9]+/g, "_") // Remplace les caractères spéciaux par _
|
||||
.replace(/^_+|_+$/g, ""); // Nettoie les _ aux bornes
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporte les données de simulation au format CSV
|
||||
*/
|
||||
export async function exportSimulationToCSV(simulationData, patientInfo) {
|
||||
if (!simulationData || !simulationData.timeline) return;
|
||||
|
||||
// \uFEFF (BOM UTF-8) pour qu'Excel ouvre le CSV directement avec les accents
|
||||
let csvContent = "\uFEFF";
|
||||
|
||||
// Métadonnées du rapport
|
||||
csvContent += `${t('csv.title')}\n`;
|
||||
if (patientInfo.name) {
|
||||
csvContent += `${t('patient.name_label') || 'Patient'}:,${patientInfo.name}\n`;
|
||||
}
|
||||
csvContent += `${t('csv.weight')}:,${patientInfo.weight}\n`;
|
||||
csvContent += `${t('csv.unit')}:,${patientInfo.infusionUnit}\n\n`;
|
||||
|
||||
// En-têtes du tableau selon la langue active
|
||||
const headers = [
|
||||
t('csv.col_time'),
|
||||
t('csv.col_bolus'),
|
||||
t('csv.col_infusion'),
|
||||
t('csv.col_domino'),
|
||||
t('csv.col_clements'),
|
||||
t('csv.col_kamp')
|
||||
];
|
||||
csvContent += headers.join(",") + "\n";
|
||||
|
||||
// Lignes de données (0 à 300 min)
|
||||
simulationData.timeline.forEach((item, index) => {
|
||||
const dominoCp = (simulationData.domino && simulationData.domino[index]) ? Math.round(simulationData.domino[index].cp) : "N/A";
|
||||
const clementsCp = (simulationData.clements && simulationData.clements[index]) ? Math.round(simulationData.clements[index].cp) : "N/A";
|
||||
const kampCp = (simulationData.kamp && simulationData.kamp[index]) ? Math.round(simulationData.kamp[index].cp) : "N/A";
|
||||
|
||||
const row = [
|
||||
item.time,
|
||||
item.bolusMg ? item.bolusMg : "0",
|
||||
item.infusionRate !== null && item.infusionRate !== undefined ? item.infusionRate : "0",
|
||||
dominoCp,
|
||||
clementsCp,
|
||||
kampCp
|
||||
];
|
||||
csvContent += row.join(",") + "\n";
|
||||
});
|
||||
|
||||
// Construction du nom de fichier : ketapk_YYYY_DD_MM_HH_MM_SS.csv ou ketapk_nom_prenom_YYYY_DD_MM_HH_MM_SS.csv
|
||||
const timestamp = getFormattedTimestamp();
|
||||
const slugName = slugify(patientInfo.name);
|
||||
const fileName = slugName
|
||||
? `ketapk_${slugName}_${timestamp}.csv`
|
||||
: `ketapk_${timestamp}.csv`;
|
||||
|
||||
// 1. Solution principale : API Native File System (Parfait sous WebView2 / Tauri Desktop)
|
||||
if (window.showSaveFilePicker) {
|
||||
try {
|
||||
const handle = await window.showSaveFilePicker({
|
||||
suggestedName: fileName,
|
||||
types: [{
|
||||
description: 'Fichier CSV',
|
||||
accept: { 'text/csv': ['.csv'] }
|
||||
}]
|
||||
});
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(csvContent);
|
||||
await writable.close();
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') return; // Annulation utilisateur
|
||||
console.warn('Fallback vers Data URI suite à l’erreur native :', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback Data URI (Compatibilité web directe sans passer par Blob)
|
||||
const encodedUri = "data:text/csv;charset=utf-8," + encodeURIComponent(csvContent);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
link.setAttribute("download", fileName);
|
||||
link.style.display = "none";
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(link)) {
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
+67
-13
@@ -68,6 +68,31 @@ export const CLINICAL_THRESHOLDS = {
|
||||
}
|
||||
};
|
||||
|
||||
// Unités de perfusion supportées
|
||||
export const INFUSION_UNITS = {
|
||||
MG_KG_H: 'mg_kg_h', // mg/kg/h
|
||||
MCG_KG_MIN: 'mcg_kg_min',// µg/kg/min
|
||||
MG_H: 'mg_h' // mg/h
|
||||
};
|
||||
|
||||
/**
|
||||
* Calcule la dose en mg administrée sur un intervalle de 5 minutes
|
||||
*/
|
||||
export function calculate5MinInfusionDose(rate, unit, weight) {
|
||||
if (!rate || rate <= 0) return 0;
|
||||
|
||||
switch (unit) {
|
||||
case INFUSION_UNITS.MG_KG_H:
|
||||
return (rate * weight) / 12;
|
||||
case INFUSION_UNITS.MCG_KG_MIN:
|
||||
return (rate * weight * 5) / 1000;
|
||||
case INFUSION_UNITS.MG_H:
|
||||
return rate / 12;
|
||||
default:
|
||||
return (rate * weight) / 12;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcul d'un bolus unique à l'instant t
|
||||
*/
|
||||
@@ -85,30 +110,59 @@ export function calculateBolusCp(model, doseMg, weightKg, timeElapsedMin) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Superposition Linéaire des Bolus et Perfusions sur 300 min
|
||||
* Propage la perfusion active sur la timeline (pas de 5 min)
|
||||
* Indique si chaque pas de temps possède une consigne explicite (isExplicit) ou héritée.
|
||||
*/
|
||||
export function generateSimulationData({ model, weightKg, timelineInputs }) {
|
||||
const timeGrid = Array.from({ length: 61 }, (_, i) => i * 5);
|
||||
const cpResults = new Array(61).fill(0);
|
||||
export function processTimelineEvents(timelineInputs, weightKg, infusionUnit) {
|
||||
let currentRate = 0;
|
||||
|
||||
timeGrid.forEach((targetTime, targetIndex) => {
|
||||
return timelineInputs.map((item) => {
|
||||
const isExplicit = item.perfRate !== undefined && item.perfRate !== null && item.perfRate !== '';
|
||||
if (isExplicit) {
|
||||
const parsed = parseFloat(item.perfRate);
|
||||
currentRate = isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
const bolusMg = parseFloat(item.bolusMg) || 0;
|
||||
const infusionMg5Min = calculate5MinInfusionDose(currentRate, infusionUnit, weightKg);
|
||||
|
||||
return {
|
||||
time: item.time,
|
||||
bolusMg: bolusMg,
|
||||
infusionRate: currentRate,
|
||||
isExplicit: isExplicit,
|
||||
infusionMg5Min: infusionMg5Min
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Superposition Linéaire des Bolus et Perfusions sur 300 min (ng/mL arrondis à l'entier)
|
||||
*/
|
||||
export function generateSimulationData({ model, weightKg, timelineInputs, infusionUnit = INFUSION_UNITS.MG_KG_H }) {
|
||||
const processedTimeline = processTimelineEvents(timelineInputs, weightKg, infusionUnit);
|
||||
const cpResults = [];
|
||||
|
||||
processedTimeline.forEach((targetEvent, targetIndex) => {
|
||||
let totalCp = 0;
|
||||
|
||||
for (let i = 0; i <= targetIndex; i++) {
|
||||
const deltaTime = targetTime - timeGrid[i];
|
||||
const input = timelineInputs[i] || { bolusMg: 0, perfRateMgKgH: 0 };
|
||||
const sourceEvent = processedTimeline[i];
|
||||
const deltaTime = targetEvent.time - sourceEvent.time;
|
||||
|
||||
if (input.bolusMg > 0) {
|
||||
totalCp += calculateBolusCp(model, input.bolusMg, weightKg, deltaTime);
|
||||
if (sourceEvent.bolusMg > 0) {
|
||||
totalCp += calculateBolusCp(model, sourceEvent.bolusMg, weightKg, deltaTime);
|
||||
}
|
||||
|
||||
if (input.perfRateMgKgH > 0) {
|
||||
const perfDoseMg = (input.perfRateMgKgH * weightKg) * (5 / 60);
|
||||
totalCp += calculateBolusCp(model, perfDoseMg, weightKg, deltaTime);
|
||||
if (sourceEvent.infusionMg5Min > 0) {
|
||||
totalCp += calculateBolusCp(model, sourceEvent.infusionMg5Min, weightKg, deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
cpResults[targetIndex] = Math.round(totalCp * 100) / 100;
|
||||
cpResults.push({
|
||||
time: targetEvent.time,
|
||||
cp: Math.round(totalCp)
|
||||
});
|
||||
});
|
||||
|
||||
return cpResults;
|
||||
|
||||
Reference in New Issue
Block a user