mirror of
https://github.com/gauvainboiche/ketapk.git
synced 2026-09-02 11:13:11 +02:00
refacto: Adding i18n support with FR and EN as base
This commit is contained in:
+18
-8
@@ -1,7 +1,8 @@
|
||||
import { MODES, PK_MODELS, CLINICAL_THRESHOLDS, generateSimulationData } from '../../pkCore.js';
|
||||
import { MODES, PK_MODELS, CLINICAL_THRESHOLDS, generateSimulationData } from '/pkCore.js';
|
||||
import { adjustChartYScale } from './chartManager.js';
|
||||
import { initCalculatorUI } from './calculator.js';
|
||||
import { generatePDFReport } from './pdfExport.js';
|
||||
import { initI18n, getCurrentLang, t } from './i18n.js';
|
||||
|
||||
let currentMode = MODES.ESKETAMINE;
|
||||
let pkChart = null;
|
||||
@@ -12,10 +13,22 @@ const timelineInputs = Array.from({ length: 61 }, (_, i) => ({
|
||||
perfRateMgKgH: 0
|
||||
}));
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await initI18n();
|
||||
|
||||
const selectLang = document.getElementById('selectLang');
|
||||
if (selectLang) {
|
||||
selectLang.value = getCurrentLang();
|
||||
selectLang.addEventListener('change', async (e) => {
|
||||
await initI18n(e.target.value);
|
||||
renderTimelineGrid();
|
||||
updateSimulation();
|
||||
});
|
||||
}
|
||||
|
||||
initChart();
|
||||
renderTimelineGrid();
|
||||
initCalculatorUI(); // Initialise la modale et les événements du calculateur
|
||||
initCalculatorUI();
|
||||
setupEventListeners();
|
||||
updateSimulation();
|
||||
});
|
||||
@@ -87,10 +100,7 @@ function updateSimulation() {
|
||||
}
|
||||
|
||||
pkChart.data.datasets = datasets;
|
||||
|
||||
// Ajustement dynamique de l'échelle Y à 110 % si besoin
|
||||
adjustChartYScale(pkChart, currentMode, datasets);
|
||||
|
||||
pkChart.update();
|
||||
}
|
||||
|
||||
@@ -104,11 +114,11 @@ function renderTimelineGrid() {
|
||||
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">Bolus (mg)</span>
|
||||
<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">
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[10px] text-slate-400 block mb-0.5">Perf (mg/kg/h)</span>
|
||||
<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">
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CalculatorEngine } from '../../pkCore.js';
|
||||
import { CalculatorEngine } from '/pkCore.js';
|
||||
|
||||
export function initCalculatorUI() {
|
||||
const modal = document.getElementById('modalCalc');
|
||||
@@ -7,7 +7,6 @@ export function initCalculatorUI() {
|
||||
|
||||
if (!modal || !btnOpen || !btnClose) return;
|
||||
|
||||
// 1. Ouverture de la modale & synchronisation avec le formulaire principal
|
||||
btnOpen.addEventListener('click', () => {
|
||||
const mainWeight = document.getElementById('inputWeight')?.value || 70;
|
||||
const mainConc = document.getElementById('inputConc')?.value || 5;
|
||||
@@ -18,19 +17,16 @@ export function initCalculatorUI() {
|
||||
modal.classList.remove('hidden');
|
||||
});
|
||||
|
||||
// 2. Fermeture via le bouton X
|
||||
btnClose.addEventListener('click', () => {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
|
||||
// 3. Fermeture au clic sur le fond sombre
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Logique de conversion en temps réel
|
||||
const calcWeightInput = document.getElementById('calcWeight');
|
||||
const mgInput = document.getElementById('calcMg');
|
||||
const mgKgInput = document.getElementById('calcMgKg');
|
||||
@@ -39,25 +35,21 @@ export function initCalculatorUI() {
|
||||
|
||||
const getWeight = () => parseFloat(calcWeightInput.value) || 70;
|
||||
|
||||
// Bolus : mg -> mg/kg
|
||||
mgInput.addEventListener('input', () => {
|
||||
const mg = parseFloat(mgInput.value);
|
||||
mgKgInput.value = isNaN(mg) ? '' : (mg / getWeight()).toFixed(2);
|
||||
});
|
||||
|
||||
// Bolus : mg/kg -> mg
|
||||
mgKgInput.addEventListener('input', () => {
|
||||
const mgKg = parseFloat(mgKgInput.value);
|
||||
mgInput.value = isNaN(mgKg) ? '' : (mgKg * getWeight()).toFixed(1);
|
||||
});
|
||||
|
||||
// Perfusion : mg/kg/h -> µg/kg/min
|
||||
mgKgHInput.addEventListener('input', () => {
|
||||
const mgKgH = parseFloat(mgKgHInput.value);
|
||||
ugKgMinInput.value = isNaN(mgKgH) ? '' : CalculatorEngine.mgKgHToMicrogKgMin(mgKgH).toFixed(1);
|
||||
});
|
||||
|
||||
// Perfusion : µg/kg/min -> mg/kg/h
|
||||
ugKgMinInput.addEventListener('input', () => {
|
||||
const ug = parseFloat(ugKgMinInput.value);
|
||||
mgKgHInput.value = isNaN(ug) ? '' : CalculatorEngine.microgKgMinToMgKgH(ug).toFixed(2);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { CLINICAL_THRESHOLDS } from '../../pkCore.js';
|
||||
import { CLINICAL_THRESHOLDS } from '/pkCore.js';
|
||||
import { t } from './i18n.js';
|
||||
|
||||
/**
|
||||
* Recalcule et applique les zones de couleur en fonction du mode et du Y max actuel
|
||||
*/
|
||||
export function updateChartAnnotations(chart, mode, currentYMax) {
|
||||
const thresholds = CLINICAL_THRESHOLDS[mode];
|
||||
const annotations = {};
|
||||
@@ -19,7 +17,7 @@ export function updateChartAnnotations(chart, mode, currentYMax) {
|
||||
borderWidth: 0,
|
||||
label: {
|
||||
display: true,
|
||||
content: zone.name,
|
||||
content: t(`zones.${zone.zoneKey}`),
|
||||
color: zone.textHex,
|
||||
font: { size: 11, weight: 'bold' },
|
||||
position: 'center'
|
||||
@@ -31,23 +29,16 @@ export function updateChartAnnotations(chart, mode, currentYMax) {
|
||||
chart.options.plugins.annotation.annotations = annotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajuste l'échelle Y de manière dynamique :
|
||||
* - Échelle par défaut (500 ou 1000)
|
||||
* - Si un pic dépasse cette valeur, l'axe monte à 110 % du pic max.
|
||||
*/
|
||||
export function adjustChartYScale(chart, mode, datasets) {
|
||||
const defaultMax = CLINICAL_THRESHOLDS[mode].maxScale;
|
||||
let highestValue = 0;
|
||||
|
||||
// Trouve la valeur la plus haute parmi toutes les courbes actives
|
||||
datasets.forEach(ds => {
|
||||
ds.data.forEach(val => {
|
||||
if (val > highestValue) highestValue = val;
|
||||
});
|
||||
});
|
||||
|
||||
// Calcul du Y max : 110 % de la valeur max si dépassement, sinon valeur par défaut
|
||||
let targetYMax = defaultMax;
|
||||
if (highestValue > defaultMax) {
|
||||
targetYMax = Math.ceil(highestValue * 1.1);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
let currentLang = localStorage.getItem('keta_lang') || (navigator.language.startsWith('en') ? 'en' : 'fr');
|
||||
let translations = {};
|
||||
|
||||
export async function initI18n(lang = currentLang) {
|
||||
currentLang = lang;
|
||||
localStorage.setItem('keta_lang', lang);
|
||||
|
||||
try {
|
||||
const response = await fetch(`./locales/${lang}.json`);
|
||||
translations = await response.json();
|
||||
translatePageDOM();
|
||||
document.documentElement.lang = lang;
|
||||
} catch (error) {
|
||||
console.error(`Erreur de chargement de la langue ${lang}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
export function translatePageDOM() {
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
const text = getNestedTranslation(key);
|
||||
if (text) el.textContent = text;
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n-placeholder');
|
||||
const text = getNestedTranslation(key);
|
||||
if (text) el.placeholder = text;
|
||||
});
|
||||
}
|
||||
|
||||
export function t(key, params = {}) {
|
||||
let val = getNestedTranslation(key) || key;
|
||||
Object.keys(params).forEach(p => {
|
||||
val = val.replace(`{${p}}`, params[p]);
|
||||
});
|
||||
return val;
|
||||
}
|
||||
|
||||
function getNestedTranslation(key) {
|
||||
return key.split('.').reduce((obj, i) => (obj ? obj[i] : null), translations);
|
||||
}
|
||||
|
||||
export function getCurrentLang() {
|
||||
return currentLang;
|
||||
}
|
||||
+22
-35
@@ -1,52 +1,39 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
import { t } from './i18n.js';
|
||||
|
||||
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');
|
||||
const doc = new jsPDF('landscape', 'mm', 'a4');
|
||||
|
||||
// --- 1. En-tête du document ---
|
||||
doc.setFillColor(30, 41, 59); // Couleur bleue nuit (slate-800)
|
||||
// En-tête
|
||||
doc.setFillColor(30, 41, 59);
|
||||
doc.rect(0, 0, 297, 25, 'F');
|
||||
|
||||
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFontSize(18);
|
||||
doc.setFontSize(16);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.text("KétaPK - Rapport de Simulation Pharmacocinétique", 14, 16);
|
||||
doc.text(t('pdf.title'), 14, 16);
|
||||
|
||||
// --- 2. Informations de la simulation ---
|
||||
// Informations Patient
|
||||
doc.setTextColor(50, 50, 50);
|
||||
doc.setFontSize(11);
|
||||
doc.setFontSize(10);
|
||||
doc.setFont("helvetica", "normal");
|
||||
|
||||
const dateStr = new Date().toLocaleString('fr-FR');
|
||||
|
||||
// Colonne de gauche (Patient)
|
||||
doc.text(`Date de simulation : ${dateStr}`, 14, 35);
|
||||
|
||||
const dateStr = new Date().toLocaleString();
|
||||
|
||||
doc.text(`${t('pdf.date')}${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);
|
||||
doc.text(`${t('pdf.mode')}${mode}`, 14, 42);
|
||||
doc.text(t('pdf.patient_info', patient), 14, 49);
|
||||
|
||||
// --- 3. Capture et insertion du Graphique ---
|
||||
// On récupère l'image du graphique sur fond blanc
|
||||
// Image du Graphique
|
||||
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);
|
||||
doc.addImage(chartImage, 'JPEG', 14, 55, 268, 120);
|
||||
|
||||
// --- 4. Pied de page ---
|
||||
// Pied de page
|
||||
doc.setFont("helvetica", "italic");
|
||||
doc.setFontSize(9);
|
||||
doc.setFontSize(8);
|
||||
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);
|
||||
doc.text(t('pdf.disclaimer'), 14, 192);
|
||||
|
||||
// --- 5. Téléchargement ---
|
||||
const fileName = `KetaPK_Rapport_${patient.weight}kg_${mode}.pdf`;
|
||||
doc.save(fileName);
|
||||
doc.save(`KetaPK_Simulation_${patient.weight}kg_${mode}.pdf`);
|
||||
}
|
||||
Reference in New Issue
Block a user