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
File diff suppressed because one or more lines are too long
+131 -70
View File
@@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="fr" class="h-full bg-slate-900 text-slate-100">
<html lang="fr" class="min-h-screen bg-slate-900 text-slate-100">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KétaPK - Simulation Pharmacocinétique</title>
<!-- CSS Tailwind compilé (Léger, rapide, hors-ligne) -->
<!-- CSS Tailwind compilé -->
<link rel="stylesheet" href="css/tailwind.css">
<!-- CSS Personnalisé -->
@@ -14,17 +15,18 @@
<!-- Librairies JS Statiques -->
<script src="vendor/chart.umd.min.js"></script>
<script src="vendor/chartjs-plugin-annotation.min.js"></script>
<script src="vendor/jspdf.umd.min.js"></script>
</head>
<body class="h-full flex flex-col font-sans antialiased selection:bg-amber-500 selection:text-slate-950">
<body class="min-h-screen flex flex-col font-sans antialiased selection:bg-amber-500 selection:text-slate-950">
<!-- NAVBAR -->
<header class="bg-slate-800/90 backdrop-blur border-b border-slate-700/80 sticky top-0 z-40 shadow-lg">
<div class="max-w-7xl mx-auto px-4 py-3 flex flex-wrap items-center justify-between gap-4">
<div class="max-w-[1800px] w-full mx-auto px-6 py-3 flex flex-wrap items-center justify-between gap-4">
<!-- Logo -->
<div class="flex items-center gap-3">
<div class="bg-gradient-to-r from-amber-500 to-amber-600 text-slate-950 font-black px-3.5 py-1 rounded-xl text-xl tracking-wider shadow-md shadow-amber-500/20">
<div
class="bg-gradient-to-r from-amber-500 to-amber-600 text-slate-950 font-black px-3.5 py-1 rounded-xl text-xl tracking-wider shadow-md shadow-amber-500/20">
KétaPK
</div>
<div class="flex flex-col">
@@ -35,10 +37,12 @@
<!-- Mode Selector -->
<div class="flex items-center bg-slate-950/80 p-1 rounded-xl border border-slate-700/80 shadow-inner">
<button id="btnEsk" type="button" data-i18n="nav.mode_esk" class="px-4 py-1.5 rounded-lg text-xs font-bold transition-all bg-amber-500 text-slate-950 shadow-md shadow-amber-500/30">
<button id="btnEsk" type="button" data-i18n="nav.mode_esk"
class="px-4 py-1.5 rounded-lg text-xs font-bold transition-all bg-amber-500 text-slate-950 shadow-md shadow-amber-500/30">
ESKETAMINE
</button>
<button id="btnRac" type="button" data-i18n="nav.mode_rac" class="px-4 py-1.5 rounded-lg text-xs font-bold transition-all text-slate-400 hover:text-slate-200">
<button id="btnRac" type="button" data-i18n="nav.mode_rac"
class="px-4 py-1.5 rounded-lg text-xs font-bold transition-all text-slate-400 hover:text-slate-200">
RACÉMIQUE
</button>
</div>
@@ -48,24 +52,26 @@
<div class="flex items-center gap-1 bg-slate-900 border border-slate-700/70 rounded-xl px-2 py-1">
<span class="text-xs">🌐</span>
<select id="selectLang" class="bg-transparent text-xs font-bold text-slate-300 outline-none cursor-pointer">
<!-- Add language here -->>
<option value="fr" class="bg-slate-900">FR</option>
<option value="en" class="bg-slate-900">EN</option>
<option value="cn" class="bg-slate-900">CN</option>
</select>
</div>
<button id="btnOpenCalc" type="button" class="bg-slate-700/80 hover:bg-slate-700 text-slate-200 border border-slate-600/60 px-3 py-1.5 rounded-lg text-xs font-medium transition flex items-center gap-1.5 shadow-sm">
<button id="btnOpenCalc" type="button"
class="bg-slate-700/80 hover:bg-slate-700 text-slate-200 border border-slate-600/60 px-3 py-1.5 rounded-lg text-xs font-medium transition flex items-center gap-1.5 shadow-sm">
<span>🧮</span>
<span data-i18n="nav.calculator" class="hidden sm:inline">Calculateur</span>
</button>
<button id="btnExportPdf" type="button" class="bg-indigo-600 hover:bg-indigo-500 text-white border border-indigo-500/50 px-3 py-1.5 rounded-lg text-xs font-semibold transition flex items-center gap-1.5 shadow-md shadow-indigo-600/20">
<span>📄</span>
<span data-i18n="nav.export_pdf" class="hidden sm:inline">Exporter PDF</span>
<button id="btnExportCSVNav" type="button"
class="bg-emerald-600/90 hover:bg-emerald-500 text-white border border-emerald-500/50 px-3 py-1.5 rounded-lg text-xs font-semibold transition flex items-center gap-1.5 shadow-md shadow-emerald-600/20">
<span>📊</span>
<span data-i18n="nav.export_csv" class="hidden sm:inline">Exporter CSV</span>
</button>
<button id="btnRaz" type="button" class="bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/30 px-3 py-1.5 rounded-lg text-xs font-medium transition flex items-center gap-1.5">
<button id="btnRaz" type="button"
class="bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/30 px-3 py-1.5 rounded-lg text-xs font-medium transition flex items-center gap-1.5">
<span>🔄</span>
<span data-i18n="nav.raz" class="hidden sm:inline">RAZ</span>
</button>
@@ -75,59 +81,78 @@
</header>
<!-- MAIN -->
<main class="flex-1 max-w-7xl w-full mx-auto p-4 grid grid-cols-1 lg:grid-cols-12 gap-6">
<main class="flex-1 max-w-[1800px] w-full mx-auto p-6 grid grid-cols-1 lg:grid-cols-12 xl:grid-cols-12 gap-6">
<!-- SIDEBAR -->
<aside class="lg:col-span-3 space-y-5">
<!-- Patient Form -->
<div class="bg-slate-800/60 border border-slate-700/70 rounded-2xl p-4 shadow-md backdrop-blur-sm">
<h2 data-i18n="patient.title" class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4 border-b border-slate-700/60 pb-2 flex items-center gap-2">
<span>👤</span> Caractéristiques Patient
</h2>
<div class="space-y-3">
<aside class="lg:col-span-3 xl:col-span-2 space-y-5">
<!-- Panneau Patient -->
<div class="bg-slate-800/80 p-4 rounded-xl border border-slate-700/50 shadow-md mb-6">
<div class="grid grid-cols-1 gap-4 items-center">
<!-- Nom du Patient -->
<div>
<label for="inputWeight" data-i18n="patient.weight" class="block text-xs text-slate-300 font-medium mb-1">Poids (kg)</label>
<input type="number" id="inputWeight" value="70" min="1" max="250" class="w-full bg-slate-900/90 border border-slate-700 rounded-xl px-3 py-2 text-sm text-white font-semibold focus:ring-2 focus:ring-amber-500 outline-none">
<label data-i18n="patient.name_label" class="block text-xs text-slate-400 mb-1 font-medium">Nom du patient</label>
<input type="text" id="patientName" data-i18n-placeholder="patient.name_placeholder" placeholder="ex: DUPONT Jean"
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-slate-100 font-bold focus:border-cyan-500 focus:outline-none">
</div>
<div class="grid grid-cols-2 gap-2">
<div>
<label for="inputAge" data-i18n="patient.age" class="block text-xs text-slate-300 font-medium mb-1">Âge (ans)</label>
<input type="number" id="inputAge" value="50" min="1" max="120" class="w-full bg-slate-900/90 border border-slate-700 rounded-xl px-3 py-2 text-sm text-white font-semibold focus:ring-2 focus:ring-amber-500 outline-none">
</div>
<div>
<label for="inputHeight" data-i18n="patient.height" class="block text-xs text-slate-300 font-medium mb-1">Taille (cm)</label>
<input type="number" id="inputHeight" value="170" min="40" max="230" class="w-full bg-slate-900/90 border border-slate-700 rounded-xl px-3 py-2 text-sm text-white font-semibold focus:ring-2 focus:ring-amber-500 outline-none">
<!-- Poids + Avertissement -->
<div>
<label data-i18n="patient.weight_label" class="block text-xs text-slate-400 mb-1 font-medium">Poids du patient (kg)</label>
<input type="number" id="patientWeight" value="70" min="10" max="250" step="any"
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-slate-100 font-bold focus:border-cyan-500 focus:outline-none">
<div id="weightWarning" class="hidden text-amber-400 text-xs mt-1.5 flex items-center gap-1 font-medium"
data-i18n="patient.weight_warning">
⚠️ Modèles pharmacocinétiques non garantis hors de la plage 30 - 120 kg.
</div>
</div>
<!-- Choix Unité Perfusion -->
<div>
<label for="inputConc" data-i18n="patient.concentration" class="block text-xs text-slate-300 font-medium mb-1">Concentration Produit (mg/mL)</label>
<input type="number" id="inputConc" value="5" step="0.1" class="w-full bg-slate-900/90 border border-slate-700 rounded-xl px-3 py-2 text-sm text-amber-400 font-bold focus:ring-2 focus:ring-amber-500 outline-none">
<label data-i18n="patient.infusion_unit_label" class="block text-xs text-slate-400 mb-1 font-medium">Unité de perfusion continue</label>
<select id="infusionUnitSelect"
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-slate-100 font-semibold focus:border-cyan-500 focus:outline-none">
<option value="mg_kg_h" data-i18n="units.mg_kg_h">mg / kg / h</option>
<option value="mcg_kg_min" data-i18n="units.mcg_kg_min">µg / kg / min</option>
<option value="mg_h" data-i18n="units.mg_h">mg / h (dose fixe)</option>
</select>
</div>
</div>
</div>
<!-- Bouton d'exportation CSV Lateral -->
<div class="flex justify-end gap-3 mb-4">
<button id="btnExportCSV" data-i18n="buttons.export_csv"
class="bg-slate-700 hover:bg-slate-600 text-slate-200 text-xs font-semibold px-3 py-2 rounded-lg flex items-center gap-2 transition w-full justify-center">
📊 Exporter valeurs CSV (ng/mL)
</button>
</div>
<!-- Models Selection -->
<div class="bg-slate-800/60 border border-slate-700/70 rounded-2xl p-4 shadow-md backdrop-blur-sm">
<h2 data-i18n="models.title" class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3 border-b border-slate-700/60 pb-2 flex items-center gap-2">
<h2 data-i18n="models.title"
class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3 border-b border-slate-700/60 pb-2 flex items-center gap-2">
<span>📈</span> Modèles Pharmacocinétiques
</h2>
<div class="space-y-2">
<label class="flex items-center justify-between p-2.5 bg-slate-900/60 border border-slate-800 rounded-xl cursor-pointer hover:bg-slate-900 transition">
<label
class="flex items-center justify-between p-2.5 bg-slate-900/60 border border-slate-800 rounded-xl cursor-pointer hover:bg-slate-900 transition">
<span class="text-xs font-bold text-orange-400">Domino et al. (1982)</span>
<input type="checkbox" id="chkDomino" checked class="w-4 h-4 accent-orange-500 rounded cursor-pointer">
</label>
<label class="flex items-center justify-between p-2.5 bg-slate-900/60 border border-slate-800 rounded-xl cursor-pointer hover:bg-slate-900 transition">
<label
class="flex items-center justify-between p-2.5 bg-slate-900/60 border border-slate-800 rounded-xl cursor-pointer hover:bg-slate-900 transition">
<span class="text-xs font-bold text-emerald-400">Clements et al. (1981)</span>
<input type="checkbox" id="chkClements" checked class="w-4 h-4 accent-emerald-500 rounded cursor-pointer">
</label>
<label class="flex items-center justify-between p-2.5 bg-slate-900/60 border border-slate-800 rounded-xl cursor-pointer hover:bg-slate-900 transition">
<label
class="flex items-center justify-between p-2.5 bg-slate-900/60 border border-slate-800 rounded-xl cursor-pointer hover:bg-slate-900 transition">
<span class="text-xs font-bold text-sky-400">Kamp et al. (2020)</span>
<input type="checkbox" id="chkKamp" checked class="w-4 h-4 accent-sky-500 rounded cursor-pointer">
</label>
@@ -137,10 +162,11 @@
</aside>
<!-- CONTENT -->
<section class="lg:col-span-9 space-y-5 flex flex-col">
<!-- Chart -->
<div class="bg-slate-800/60 border border-slate-700/70 rounded-2xl p-4 shadow-md backdrop-blur-sm flex flex-col h-[420px] relative">
<section class="lg:col-span-9 xl:col-span-10 space-y-5 flex flex-col">
<!-- Graphique dynamique -->
<div
class="bg-slate-800/60 border border-slate-700/70 rounded-2xl p-4 shadow-md backdrop-blur-sm flex flex-col h-[calc(100vh-370px)] min-h-[450px] relative">
<div class="flex-1 w-full h-full relative">
<canvas id="pkChart"></canvas>
</div>
@@ -149,74 +175,108 @@
<!-- Timeline Grid -->
<div class="bg-slate-800/60 border border-slate-700/70 rounded-2xl p-4 shadow-md backdrop-blur-sm">
<div class="flex items-center justify-between mb-3 border-b border-slate-700/60 pb-2">
<h2 data-i18n="timeline.title" class="text-xs font-bold text-slate-300 uppercase tracking-wider flex items-center gap-2">
<h2 data-i18n="timeline.title"
class="text-xs font-bold text-slate-300 uppercase tracking-wider flex items-center gap-2">
<span>⏱️</span> Saisie Chronologique Doses & Perfusion (0 - 300 min)
</h2>
<span data-i18n="timeline.scroll_hint" class="text-[10px] text-slate-400 italic">Défilement horizontal 👉</span>
</div>
<div id="timelineGrid" class="flex gap-2 overflow-x-auto pb-2 scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-slate-900">
<!-- Injected via app.js -->
<div id="timelineGrid"
class="flex gap-2 overflow-x-auto pb-2 scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-slate-900">
<!-- Injecté via app.js -->
</div>
</div>
<!-- Tableau de contrôle des concentrations (0-300 min) -->
<details class="bg-slate-800/50 border border-slate-700/50 rounded-xl mb-6">
<summary
class="p-4 cursor-pointer text-slate-300 font-semibold text-sm hover:text-white flex justify-between items-center">
<span data-i18n="table.summary_title">🔍 Tableau de contrôle des concentrations calculées (0 - 300 min)</span>
<span data-i18n="table.summary_subtitle" class="text-xs text-slate-500">Cliquer pour afficher/masquer</span>
</summary>
<div class="p-4 border-t border-slate-700/50 overflow-x-auto max-h-80">
<table class="w-full text-xs text-left text-slate-300">
<thead class="text-slate-400 bg-slate-900/80 uppercase font-mono sticky top-0">
<tr>
<th data-i18n="table.col_time" class="px-3 py-2">Temps</th>
<th data-i18n="table.col_bolus" class="px-3 py-2">Bolus (mg)</th>
<th data-i18n="table.col_infusion" class="px-3 py-2">Perf. Active</th>
<th data-i18n="table.col_domino" class="px-3 py-2 text-cyan-400">Domino (ng/mL)</th>
<th data-i18n="table.col_clements" class="px-3 py-2 text-emerald-400">Clements (ng/mL)</th>
<th data-i18n="table.col_kamp" class="px-3 py-2 text-purple-400">Kamp (ng/mL)</th>
</tr>
</thead>
<tbody id="dataTableBody" class="divide-y divide-slate-800 font-mono">
<!-- Généré dynamiquement en JS -->
</tbody>
</table>
</div>
</details>
</section>
</main>
<!-- MODAL CALCULATOR -->
<div id="modalCalc" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4 transition-opacity">
<div id="modalCalc"
class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4 transition-opacity">
<div class="bg-slate-800 border border-slate-700 rounded-2xl w-full max-w-lg p-6 space-y-6 shadow-2xl relative">
<div class="flex justify-between items-center border-b border-slate-700 pb-3">
<h3 data-i18n="calculator.title" class="text-base font-bold text-amber-400 flex items-center gap-2">
<span>🧮</span> Calculateur & Conversions de Doses
</h3>
<button id="btnCloseCalc" type="button" class="text-slate-400 hover:text-white font-bold text-xl leading-none transition">&times;</button>
<button id="btnCloseCalc" type="button"
class="text-slate-400 hover:text-white font-bold text-xl leading-none transition">&times;</button>
</div>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-3 bg-slate-900/80 p-3 rounded-xl border border-slate-700/50">
<div>
<label for="calcWeight" data-i18n="calculator.patient_weight" class="block text-xs text-slate-400 mb-1">Poids du patient</label>
<label for="calcWeight" data-i18n="calculator.patient_weight"
class="block text-xs text-slate-400 mb-1">Poids du patient</label>
<div class="flex items-center gap-1">
<input type="number" id="calcWeight" value="70" class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2 text-sm text-white font-bold outline-none">
<input type="number" id="calcWeight" value="70" step="any"
class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2 text-sm text-white font-bold outline-none">
<span class="text-xs text-slate-400">kg</span>
</div>
</div>
<div>
<label for="calcConc" data-i18n="calculator.drug_conc" class="block text-xs text-slate-400 mb-1">Concentration</label>
<div class="flex items-center gap-1">
<input type="number" id="calcConc" value="10" class="w-full bg-slate-800 border border-slate-700 rounded-lg p-2 text-sm text-amber-400 font-bold outline-none">
<span class="text-xs text-slate-400">mg/mL</span>
</div>
</div>
</div>
<div>
<h4 data-i18n="calculator.bolus_header" class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Conversion Bolus</h4>
<h4 data-i18n="calculator.bolus_header"
class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Conversion Bolus</h4>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="calcMg" data-i18n="calculator.total_dose" class="block text-[10px] text-slate-500 mb-1">Dose totale (mg)</label>
<input type="number" id="calcMg" placeholder="ex: 35" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:ring-1 focus:ring-amber-500 outline-none">
<input type="number" id="calcMg" step="any" placeholder="ex: 35"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:ring-1 focus:ring-amber-500 outline-none">
</div>
<div>
<label for="calcMgKg" data-i18n="calculator.relative_dose" class="block text-[10px] text-slate-500 mb-1">Dose relative (mg/kg)</label>
<input type="number" id="calcMgKg" placeholder="ex: 0.5" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:ring-1 focus:ring-amber-500 outline-none">
<label for="calcMgKg" data-i18n="calculator.relative_dose"
class="block text-[10px] text-slate-500 mb-1">Dose relative (mg/kg)</label>
<input type="number" id="calcMgKg" step="any" placeholder="ex: 0.5"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:ring-1 focus:ring-amber-500 outline-none">
</div>
</div>
</div>
<div>
<h4 data-i18n="calculator.infusion_header" class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Conversion Perfusion</h4>
<h4 data-i18n="calculator.infusion_header"
class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Conversion Perfusion</h4>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="calcMgKgH" data-i18n="calculator.rate_mgkgh" class="block text-[10px] text-slate-500 mb-1">Débit (mg/kg/h)</label>
<input type="number" id="calcMgKgH" placeholder="ex: 0.3" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:ring-1 focus:ring-amber-500 outline-none">
<label for="calcMgKgH" data-i18n="calculator.rate_mgkgh"
class="block text-[10px] text-slate-500 mb-1">Débit (mg/kg/h)</label>
<input type="number" id="calcMgKgH" step="any" placeholder="ex: 0.3"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:ring-1 focus:ring-amber-500 outline-none">
</div>
<div>
<label for="calcUgKgMin" data-i18n="calculator.rate_ugkgmin" class="block text-[10px] text-slate-500 mb-1">Débit (µg/kg/min)</label>
<input type="number" id="calcUgKgMin" placeholder="ex: 5" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:ring-1 focus:ring-amber-500 outline-none">
<label for="calcUgKgMin" data-i18n="calculator.rate_ugkgmin"
class="block text-[10px] text-slate-500 mb-1">Débit (µg/kg/min)</label>
<input type="number" id="calcUgKgMin" step="any" placeholder="ex: 5"
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-white focus:ring-1 focus:ring-amber-500 outline-none">
</div>
</div>
</div>
@@ -227,4 +287,5 @@
<script type="module" src="js/app.js"></script>
</body>
</html>
+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();
}
+8 -9
View File
@@ -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);
});
+118
View File
@@ -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 à lerreur 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
View File
@@ -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;
+60 -38
View File
@@ -1,56 +1,78 @@
{
"app": {
"title": "药代动力学",
"version": "v1.00 • 乔治·米翁博士"
"title": "药代动力学模拟",
"version": "v1.00 • Dr Georges Mion"
},
"nav": {
"mode_esk": "艾斯克塔明",
"mode_rac": "消旋",
"mode_esk": "右旋氯胺酮",
"mode_rac": "消旋氯胺酮",
"calculator": "计算器",
"export_pdf": "导出 PDF",
"export_csv": "导出 CSV",
"raz": "重置"
},
"patient": {
"title": "患者参数",
"weight": "体重 (kg)",
"age": "年龄 (岁)",
"height": "身高 (cm)",
"concentration": "药物浓度 (mg/mL)"
"name_label": "患者姓名",
"name_placeholder": "例如:张三",
"weight_label": "患者体重 (kg)",
"weight_warning": "⚠️ 药代动力学模型仅在 30 - 120 kg 范围内保证准确性。",
"infusion_unit_label": "连续输注单位"
},
"models": {
"title": "药代动力学模型"
"units": {
"mg_kg_h": "mg / kg / h",
"mcg_kg_min": "µg / kg / min",
"mg_h": "mg / h (固定剂量)"
},
"timeline": {
"title": "给药与输液时间线 (0 - 300 分钟)",
"scroll_hint": "水平滚动 👉",
"bolus": "bolus 剂量 (mg)",
"perf": "输注速率 (mg/kg/h)"
"buttons": {
"export_csv": "📊 导出 CSV 数据 (ng/mL)"
},
"calculator": {
"title": "计算器与剂量换算",
"patient_weight": "患者体重",
"drug_conc": "浓度",
"bolus_header": "推注剂量换算",
"total_dose": "总剂量 (mg)",
"relative_dose": "相对剂量 (mg/kg)",
"infusion_header": "输液换算",
"rate_mgkgh": "速率 (mg/kg/h)",
"rate_ugkgmin": "速率 (µg/kg/min)"
"table": {
"summary_title": "🔍 浓度计算控制表 (0 - 300 min)",
"summary_subtitle": "点击展开/折叠",
"col_time": "时间",
"col_bolus": "负荷剂量 (mg)",
"col_infusion": "有效输注",
"col_domino": "Domino (ng/mL)",
"col_clements": "Clements (ng/mL)",
"col_kamp": "Kamp (ng/mL)"
},
"csv": {
"title": "KetaPK - 原始数据报告",
"weight": "患者体重 (kg)",
"unit": "输注单位",
"col_time": "时间 (min)",
"col_bolus": "负荷剂量 (mg)",
"col_infusion": "有效输注速率",
"col_domino": "Domino (ng/mL)",
"col_clements": "Clements (ng/mL)",
"col_kamp": "Kamp (ng/mL)"
},
"zones": {
"inactive": "活性",
"anti_hyperalgesic": "抗痛觉过敏",
"analgesia": "镇痛",
"psychedelic": "致幻",
"emergence": "苏醒",
"inactive": "活性",
"anti_hyperalgesic": "抗痛觉过敏",
"analgesia": "镇痛",
"psychedelic": "致幻",
"emergence": "苏醒",
"narcosis": "麻醉",
"excessive": "过量"
},
"pdf": {
"title": "KetaPK - 药代动力学模拟报告",
"date": "模拟日期:",
"mode": "所选模式:",
"patient_info": "患者:体重 {weight} 公斤 | 年龄 {age} 岁 | 身高 {height} 厘米",
"disclaimer": "由 KetaPK Web 生成。请仔细核对"
"models": {
"title": "📈 药代动力学模型"
},
"timeline": {
"title": "⏱️ 给药与输注按时间录入 (0 - 300 min)",
"scroll_hint": "横向滚动 👉",
"bolus": "负荷剂量 (mg)",
"perf": "输注速率"
},
"calculator": {
"title": "计算器与剂量转换",
"patient_weight": "患者体重",
"drug_conc": "浓度",
"bolus_header": "负荷剂量转换",
"total_dose": "总剂量 (mg)",
"relative_dose": "相对剂量 (mg/kg)",
"infusion_header": "输注速率转换",
"rate_mgkgh": "速率 (mg/kg/h)",
"rate_ugkgmin": "速率 (µg/kg/min)"
}
}
+52 -30
View File
@@ -7,50 +7,72 @@
"mode_esk": "ESKETAMINE",
"mode_rac": "RACEMIC",
"calculator": "Calculator",
"export_pdf": "Export PDF",
"export_csv": "Export CSV",
"raz": "Reset"
},
"patient": {
"title": "Patient Parameters",
"weight": "Weight (kg)",
"age": "Age (years)",
"height": "Height (cm)",
"concentration": "Drug Concentration (mg/mL)"
"name_label": "Patient Name",
"name_placeholder": "e.g. SMITH John",
"weight_label": "Patient Weight (kg)",
"weight_warning": "⚠️ Pharmacokinetic models are not guaranteed outside the 30 - 120 kg range.",
"infusion_unit_label": "Continuous infusion unit"
},
"models": {
"title": "PK Models"
"units": {
"mg_kg_h": "mg / kg / h",
"mcg_kg_min": "µg / kg / min",
"mg_h": "mg / h (fixed dose)"
},
"timeline": {
"title": "Chronological Dosing & Infusion (0 - 300 min)",
"scroll_hint": "Horizontal scroll 👉",
"bolus": "Bolus (mg)",
"perf": "Infusion (mg/kg/h)"
"buttons": {
"export_csv": "📊 Export CSV values (ng/mL)"
},
"calculator": {
"title": "Calculator & Dose Conversions",
"patient_weight": "Patient Weight",
"drug_conc": "Concentration",
"bolus_header": "Bolus Conversion",
"total_dose": "Total Dose (mg)",
"relative_dose": "Relative Dose (mg/kg)",
"infusion_header": "Infusion Conversion",
"rate_mgkgh": "Rate (mg/kg/h)",
"rate_ugkgmin": "Rate (µg/kg/min)"
"table": {
"summary_title": "🔍 Concentration Control Table (0 - 300 min)",
"summary_subtitle": "Click to expand/collapse",
"col_time": "Time",
"col_bolus": "Bolus (mg)",
"col_infusion": "Active Infusion",
"col_domino": "Domino (ng/mL)",
"col_clements": "Clements (ng/mL)",
"col_kamp": "Kamp (ng/mL)"
},
"csv": {
"title": "KetaPK - Raw Data Report",
"weight": "Patient weight (kg)",
"unit": "Infusion unit",
"col_time": "Time (min)",
"col_bolus": "Bolus (mg)",
"col_infusion": "Active infusion",
"col_domino": "Domino (ng/mL)",
"col_clements": "Clements (ng/mL)",
"col_kamp": "Kamp (ng/mL)"
},
"zones": {
"inactive": "Inactive",
"anti_hyperalgesic": "Anti-Hyperalgesic",
"anti_hyperalgesic": "Anti-hyperalgesic",
"analgesia": "Analgesia",
"psychedelic": "Psychedelic",
"emergence": "Emergence",
"narcosis": "Narcosis",
"excessive": "Excessive"
},
"pdf": {
"title": "KetaPK - Pharmacokinetic Simulation Report",
"date": "Simulation Date: ",
"mode": "Selected Mode: ",
"patient_info": "Patient: Weight {weight} kg | Age {age} yrs | Height {height} cm",
"disclaimer": "Generated by KetaPK Web. Please double-check."
"models": {
"title": "Pharmacokinetic Models"
},
"timeline": {
"title": "Chronological Doses & Infusion Entry (0 - 300 min)",
"scroll_hint": "Horizontal scroll 👉",
"bolus": "Bolus (mg)",
"perf": "Infusion rate"
},
"calculator": {
"title": "Calculator & Dose Conversions",
"patient_weight": "Patient Weight",
"drug_conc": "Concentration",
"bolus_header": "Bolus Conversion",
"total_dose": "Total dose (mg)",
"relative_dose": "Relative dose (mg/kg)",
"infusion_header": "Infusion Conversion",
"rate_mgkgh": "Rate (mg/kg/h)",
"rate_ugkgmin": "Rate (µg/kg/min)"
}
}
+45 -23
View File
@@ -7,15 +7,53 @@
"mode_esk": "ESKETAMINE",
"mode_rac": "RACÉMIQUE",
"calculator": "Calculateur",
"export_pdf": "Exporter PDF",
"export_csv": "Exporter CSV",
"raz": "RAZ"
},
"patient": {
"title": "Caractéristiques Patient",
"weight": "Poids (kg)",
"age": "Âge (ans)",
"height": "Taille (cm)",
"concentration": "Concentration Produit (mg/mL)"
"name_label": "Nom du patient",
"name_placeholder": "ex: DUPONT Jean",
"weight_label": "Poids du patient (kg)",
"weight_warning": "⚠️ Modèles pharmacocinétiques non garantis hors de la plage 30 - 120 kg.",
"infusion_unit_label": "Unité de perfusion continue"
},
"units": {
"mg_kg_h": "mg / kg / h",
"mcg_kg_min": "µg / kg / min",
"mg_h": "mg / h (dose fixe)"
},
"buttons": {
"export_csv": "📊 Exporter valeurs CSV (ng/mL)"
},
"table": {
"summary_title": "🔍 Tableau de contrôle des concentrations calculées (0 - 300 min)",
"summary_subtitle": "Cliquer pour afficher/masquer",
"col_time": "Temps",
"col_bolus": "Bolus (mg)",
"col_infusion": "Perf. Active",
"col_domino": "Domino (ng/mL)",
"col_clements": "Clements (ng/mL)",
"col_kamp": "Kamp (ng/mL)"
},
"csv": {
"title": "KetaPK - Rapport de donnees brutes",
"weight": "Poids du patient (kg)",
"unit": "Unite de perfusion",
"col_time": "Temps (min)",
"col_bolus": "Bolus (mg)",
"col_infusion": "Perfusion active",
"col_domino": "Domino (ng/mL)",
"col_clements": "Clements (ng/mL)",
"col_kamp": "Kamp (ng/mL)"
},
"zones": {
"inactive": "Inactif",
"anti_hyperalgesic": "Anti-hyperalgésique",
"analgesia": "Analgésie",
"psychedelic": "Psychédélique",
"emergence": "Émergence",
"narcosis": "Narcose",
"excessive": "Excessif"
},
"models": {
"title": "Modèles Pharmacocinétiques"
@@ -24,7 +62,7 @@
"title": "Saisie Chronologique Doses & Perfusion (0 - 300 min)",
"scroll_hint": "Défilement horizontal 👉",
"bolus": "Bolus (mg)",
"perf": "Perf (mg/kg/h)"
"perf": "Débit perf."
},
"calculator": {
"title": "Calculateur & Conversions de Doses",
@@ -36,21 +74,5 @@
"infusion_header": "Conversion Perfusion",
"rate_mgkgh": "Débit (mg/kg/h)",
"rate_ugkgmin": "Débit (µg/kg/min)"
},
"zones": {
"inactive": "Inactif",
"anti_hyperalgesic": "Anti-Hyperalgésique",
"analgesia": "Analgésie",
"psychedelic": "Psychédélique",
"emergence": "Émergence",
"narcosis": "Narcose",
"excessive": "Excessif"
},
"pdf": {
"title": "KétaPK - Rapport de Simulation Pharmacocinétique",
"date": "Date de simulation : ",
"mode": "Mode sélectionné : ",
"patient_info": "Patient : Poids {weight} kg | Âge {age} ans | Taille {height} cm",
"disclaimer": "Ce document est généré par KétaPK Web. Veuillez vérifier les sorties avant toute action."
}
}