feat: Creating the base of application

This commit is contained in:
gauvainboiche
2026-08-01 17:34:23 +02:00
parent c84d6f05ec
commit 7a7815f3bd
11 changed files with 773 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
import { MODES, PK_MODELS, CLINICAL_THRESHOLDS, generateSimulationData } from '../../pkCore.js';
import { generatePDFReport } from './pdfExport.js';
let currentMode = MODES.ESKETAMINE;
let pkChart = null;
// Initialisation de la Grille Saisie (61 intervalles de 5 min)
const timelineInputs = Array.from({ length: 61 }, (_, i) => ({
time: i * 5,
bolusMg: 0,
perfRateMgKgH: 0
}));
document.addEventListener('DOMContentLoaded', () => {
initChart();
renderTimelineGrid();
setupEventListeners();
updateSimulation();
});
function initChart() {
const ctx = document.getElementById('pkChart').getContext('2d');
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: '#94a3b8' },
grid: { color: 'rgba(51, 65, 85, 0.3)' }
},
x: {
title: { display: true, text: 'Temps (minutes)', color: '#94a3b8' },
grid: { color: 'rgba(51, 65, 85, 0.3)' }
}
},
plugins: {
legend: { labels: { color: '#f8fafc' } }
}
}
});
}
function updateSimulation() {
const weight = parseFloat(document.getElementById('inputWeight').value) || 70;
const datasets = [];
if (document.getElementById('chkDomino').checked) {
datasets.push({
label: PK_MODELS.DOMINO.name,
data: generateSimulationData({ model: PK_MODELS.DOMINO, weightKg: weight, timelineInputs }),
borderColor: PK_MODELS.DOMINO.color,
borderWidth: 3,
tension: 0.2
});
}
if (document.getElementById('chkClements').checked) {
datasets.push({
label: PK_MODELS.CLEMENTS.name,
data: generateSimulationData({ model: PK_MODELS.CLEMENTS, weightKg: weight, timelineInputs }),
borderColor: PK_MODELS.CLEMENTS.color,
borderWidth: 3,
tension: 0.2
});
}
if (document.getElementById('chkKamp').checked) {
datasets.push({
label: PK_MODELS.KAMP.name,
data: generateSimulationData({ model: PK_MODELS.KAMP, weightKg: weight, timelineInputs }),
borderColor: PK_MODELS.KAMP.color,
borderWidth: 3,
tension: 0.2
});
}
pkChart.data.datasets = datasets;
pkChart.options.scales.y.max = CLINICAL_THRESHOLDS[currentMode].maxScale;
pkChart.update();
}
function renderTimelineGrid() {
const container = document.getElementById('timelineGrid');
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';
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-500 block">Bolus (mg)</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">
</div>
<div>
<span class="text-[10px] text-slate-500 block">Perf (mg/kg/h)</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">
</div>
`;
container.appendChild(col);
});
}
function setupEventListeners() {
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;
updateSimulation();
}
});
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);
});
document.getElementById('btnRaz').addEventListener('click', () => {
timelineInputs.forEach(item => { item.bolusMg = 0; item.perfRateMgKgH = 0; });
renderTimelineGrid();
updateSimulation();
});
document.getElementById('btnExportPdf').addEventListener('click', () => {
// On rassemble les données actuelles du formulaire
const patientData = {
weight: document.getElementById('inputWeight').value || 70,
age: document.getElementById('inputAge').value || 50,
height: document.getElementById('inputHeight').value || 170
};
// On lance l'exportation (pkChart et currentMode sont des variables globales de app.js)
generatePDFReport(pkChart, patientData, currentMode);
});
}
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-sm font-semibold bg-amber-500 text-slate-950 shadow';
btnRac.className = 'px-4 py-1.5 rounded-lg text-sm font-semibold text-slate-300 hover:text-white';
concInput.value = CLINICAL_THRESHOLDS[MODES.ESKETAMINE].defaultConcentration;
} else {
btnRac.className = 'px-4 py-1.5 rounded-lg text-sm font-semibold bg-fuchsia-500 text-slate-950 shadow';
btnEsk.className = 'px-4 py-1.5 rounded-lg text-sm font-semibold text-slate-300 hover:text-white';
concInput.value = CLINICAL_THRESHOLDS[MODES.RACEMIQUE].defaultConcentration;
}
updateSimulation();
}
+38
View File
@@ -0,0 +1,38 @@
import { CalculatorEngine } from '../pkCore.js';
export function initCalculatorUI() {
const weightInput = document.getElementById('calcWeight');
const concInput = document.getElementById('calcConc');
// Conversion Bolus
const mgInput = document.getElementById('calcMg');
const mgKgInput = document.getElementById('calcMgKg');
// Conversion Perfusion
const mgKgHInput = document.getElementById('calcMgKgH');
const ugKgMinInput = document.getElementById('calcUgKgMin');
// 1. Bolus mg <-> mg/kg
mgInput.addEventListener('input', () => {
const w = parseFloat(weightInput.value) || 70;
const mg = parseFloat(mgInput.value) || 0;
mgKgInput.value = CalculatorEngine.mgToMgKg(mg, w).toFixed(2);
});
mgKgInput.addEventListener('input', () => {
const w = parseFloat(weightInput.value) || 70;
const mgKg = parseFloat(mgKgInput.value) || 0;
mgInput.value = CalculatorEngine.mgKgToMg(mgKg, w).toFixed(1);
});
// 2. Vitesse mg/kg/h <-> µg/kg/min
mgKgHInput.addEventListener('input', () => {
const mgKgH = parseFloat(mgKgHInput.value) || 0;
ugKgMinInput.value = CalculatorEngine.mgKgHToMicrogKgMin(mgKgH).toFixed(1);
});
ugKgMinInput.addEventListener('input', () => {
const ugKgMin = parseFloat(ugKgMinInput.value) || 0;
mgKgHInput.value = CalculatorEngine.microgKgMinToMgKgH(ugKgMin).toFixed(2);
});
}
+30
View File
@@ -0,0 +1,30 @@
import { CLINICAL_THRESHOLDS } from '../pkCore.js';
export function createChartAnnotations(mode) {
const thresholds = CLINICAL_THRESHOLDS[mode];
const annotations = {};
thresholds.zones.forEach((zone, index) => {
// Évite d'étendre la dernière zone à l'infini sur le graphe
const yMax = zone.max === Infinity ? thresholds.maxScale : zone.max;
if (zone.min < thresholds.maxScale) {
annotations[`zone_${index}`] = {
type: 'box',
yMin: zone.min,
yMax: Math.min(yMax, thresholds.maxScale),
backgroundColor: zone.color,
borderWidth: 0,
label: {
display: true,
content: zone.name,
color: zone.textHex,
font: { size: 14, weight: 'bold' },
position: 'center'
}
};
}
});
return annotations;
}
+52
View File
@@ -0,0 +1,52 @@
/**
* Génère et télécharge un rapport PDF de la simulation.
* @param {Object} pkChart - L'instance Chart.js actuelle
* @param {Object} patient - Les infos du patient { weight, age, height }
* @param {String} mode - ESKETAMINE ou RACEMIQUE
*/
export function generatePDFReport(pkChart, patient, mode) {
// jsPDF est chargé globalement via le CDN dans index.html
const { jsPDF } = window.jspdf;
// Format A4, orientation Paysage (landscape) idéal pour les graphiques
const doc = new jsPDF('landscape', 'mm', 'a4');
// --- 1. En-tête du document ---
doc.setFillColor(30, 41, 59); // Couleur bleue nuit (slate-800)
doc.rect(0, 0, 297, 25, 'F');
doc.setTextColor(255, 255, 255);
doc.setFontSize(18);
doc.setFont("helvetica", "bold");
doc.text("KétaPK - Rapport de Simulation Pharmacocinétique", 14, 16);
// --- 2. Informations de la simulation ---
doc.setTextColor(50, 50, 50);
doc.setFontSize(11);
doc.setFont("helvetica", "normal");
const dateStr = new Date().toLocaleString('fr-FR');
// Colonne de gauche (Patient)
doc.text(`Date de simulation : ${dateStr}`, 14, 35);
doc.setFont("helvetica", "bold");
doc.text(`Mode sélectionné : ${mode}`, 14, 42);
doc.text(`Patient : Poids ${patient.weight} kg | Âge ${patient.age} ans | Taille ${patient.height} cm`, 14, 49);
// --- 3. Capture et insertion du Graphique ---
// On récupère l'image du graphique sur fond blanc
const chartImage = pkChart.toBase64Image('image/jpeg', 1.0);
// Placement : x=14mm, y=60mm, largeur=260mm, hauteur=110mm
doc.addImage(chartImage, 'JPEG', 14, 60, 260, 110);
// --- 4. Pied de page ---
doc.setFont("helvetica", "italic");
doc.setFontSize(9);
doc.setTextColor(150, 150, 150);
doc.text("Ce document est généré par KétaPK Web. Il est destiné uniquement à l'enseignement et ne doit pas être utilisé pour la conduite thérapeutique.", 14, 190);
// --- 5. Téléchargement ---
const fileName = `KetaPK_Rapport_${patient.weight}kg_${mode}.pdf`;
doc.save(fileName);
}