Add localization support and docs

This commit is contained in:
2025-10-18 21:44:18 +02:00
parent 0fb168b050
commit 4166cf0460
15 changed files with 424 additions and 110 deletions

View File

@@ -0,0 +1,82 @@
# Medication Plan Assistant - Modular Structure
This application has been successfully modularized to simplify maintenance and further development.
## 📁 New Project Structure
```text
src/
├── App.js # Main component (greatly simplified)
├── components/ # UI components
│ ├── DoseSchedule.js # Dosage schedule input
│ ├── DeviationList.js # Deviations from the schedule
│ ├── SuggestionPanel.js # Correction suggestions
│ ├── SimulationChart.js # Chart component
│ ├── Settings.js # Settings panel
│ ├── TimeInput.js # Time input with picker
│ └── NumericInput.js # Numeric input with +/- buttons
├── hooks/ # Custom React hooks
│ ├── useAppState.js # State management & local storage
│ └── useSimulation.js # Simulation logic & calculations
├── utils/ # Utility functions
│ ├── timeUtils.js # Time utility functions
│ ├── pharmacokinetics.js # PK calculations
│ ├── calculations.js # Concentration calculations
│ └── suggestions.js # Correction suggestion algorithm
└── constants/
└── defaults.js # Constants & default values
```
## 🎯 Advantages of the new structure
### ✅ Better maintainability
- **Small, focused modules**: Each file has a clear responsibility
- **Easier debugging**: Problems can be localized more quickly
- **Clearer code organization**: Related functions are grouped together
### ✅ Reusability
- **UI components**: `TimeInput`, `NumericInput` can be used anywhere
- **Utility functions**: PK calculations are isolated and testable
- **Custom hooks**: State logic is reusable
### ✅ Development friendliness
- **Parallel development**: Teams can work on different modules
- **Simpler testing**: Each module can be tested in isolation
- **Better IDE support**: Smaller files = better performance
### ✅ Scalability
- **New features**: Easy to add as new modules
- **Code splitting**: Possible for better performance
- **Refactoring**: Changes are limited locally
## 🔧 Technical details
### State management
- **useAppState**: Manages global app state and LocalStorage
- **useSimulation**: Manages all simulation-related calculations
### Component architecture
- **Dumb components**: UI components receive props and call callbacks
- **Smart Components**: Hooks manage state and business logic
- **Separation of Concerns**: UI, state, and business logic are separated
### Utils & Calculations
- **Pure functions**: All calculations are side-effect-free
- **Modular exports**: Functions can be imported individually
- **Typed interfaces**: Clear input/output definitions
## 🚀 Migration complete
The application works identically to the original version, but now:
- Split into **350+ lines** across **several small modules**
- **Easier to understand** and modify
- **Ready for further features** and improvements
- **More test-friendly** for unit and integration tests

View File

@@ -6,36 +6,40 @@ import DeviationList from './components/DeviationList.js';
import SuggestionPanel from './components/SuggestionPanel.js'; import SuggestionPanel from './components/SuggestionPanel.js';
import SimulationChart from './components/SimulationChart.js'; import SimulationChart from './components/SimulationChart.js';
import Settings from './components/Settings.js'; import Settings from './components/Settings.js';
import LanguageSelector from './components/LanguageSelector.js';
// Custom Hooks // Custom Hooks
import { useAppState } from './hooks/useAppState.js'; import { useAppState } from './hooks/useAppState.js';
import { useSimulation } from './hooks/useSimulation.js'; import { useSimulation } from './hooks/useSimulation.js';
import { useLanguage } from './hooks/useLanguage.js';
// --- Main Component --- // --- Main Component ---
const MedPlanAssistant = () => { const MedPlanAssistant = () => {
const { const { currentLanguage, t, changeLanguage } = useLanguage();
appState,
updateState, const {
updateNestedState, appState,
updateUiSetting, updateState,
handleReset updateNestedState,
updateUiSetting,
handleReset
} = useAppState(); } = useAppState();
const { const {
pkParams, pkParams,
doses, doses,
therapeuticRange, therapeuticRange,
doseIncrement, doseIncrement,
uiSettings uiSettings
} = appState; } = appState;
const { const {
showDayTimeXAxis, showDayTimeXAxis,
chartView, chartView,
yAxisMin, yAxisMin,
yAxisMax, yAxisMax,
simulationDays, simulationDays,
displayedDays displayedDays
} = uiSettings; } = uiSettings;
const { const {
@@ -54,57 +58,65 @@ const MedPlanAssistant = () => {
<div className="bg-gray-100 font-sans p-4 sm:p-6 lg:p-8"> <div className="bg-gray-100 font-sans p-4 sm:p-6 lg:p-8">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<header className="mb-8"> <header className="mb-8">
<h1 className="text-3xl md:text-4xl font-bold text-gray-800">Medikationsplan-Assistent</h1> <div className="flex justify-between items-start">
<p className="text-gray-600 mt-1">Simulation für Lisdexamfetamin (LDX) und d-Amphetamin (d-amph)</p> <div>
<h1 className="text-3xl md:text-4xl font-bold text-gray-800">{t.appTitle}</h1>
<p className="text-gray-600 mt-1">{t.appSubtitle}</p>
</div>
<LanguageSelector currentLanguage={currentLanguage} onLanguageChange={changeLanguage} t={t} />
</div>
</header> </header>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Controls */} {/* Left Column - Controls */}
<div className="lg:col-span-1 space-y-6 lg:order-1"> <div className="lg:col-span-1 space-y-6 lg:order-1">
<DoseSchedule <DoseSchedule
doses={doses} doses={doses}
doseIncrement={doseIncrement} doseIncrement={doseIncrement}
onUpdateDoses={(newDoses) => updateState('doses', newDoses)} onUpdateDoses={(newDoses) => updateState('doses', newDoses)}
t={t}
/> />
<DeviationList <DeviationList
deviations={deviations} deviations={deviations}
doseIncrement={doseIncrement} doseIncrement={doseIncrement}
simulationDays={simulationDays} simulationDays={simulationDays}
onAddDeviation={addDeviation} onAddDeviation={addDeviation}
onRemoveDeviation={removeDeviation} onRemoveDeviation={removeDeviation}
onDeviationChange={handleDeviationChange} onDeviationChange={handleDeviationChange}
t={t}
/> />
<SuggestionPanel <SuggestionPanel
suggestion={suggestion} suggestion={suggestion}
onApplySuggestion={applySuggestion} onApplySuggestion={applySuggestion}
t={t}
/> />
</div> </div>
{/* Center Column - Chart */} {/* Center Column - Chart */}
<div className="lg:col-span-2 bg-white p-5 rounded-lg shadow-sm border min-h-[600px] flex flex-col lg:order-2"> <div className="lg:col-span-2 bg-white p-5 rounded-lg shadow-sm border min-h-[600px] flex flex-col lg:order-2">
<div className="flex justify-center space-x-2 mb-4"> <div className="flex justify-center space-x-2 mb-4">
<button <button
onClick={() => updateUiSetting('chartView', 'damph')} onClick={() => updateUiSetting('chartView', 'damph')}
className={`px-4 py-2 text-sm font-medium rounded-md ${chartView === 'damph' ? 'bg-sky-600 text-white' : 'bg-gray-200 text-gray-700'}`} className={`px-4 py-2 text-sm font-medium rounded-md ${chartView === 'damph' ? 'bg-sky-600 text-white' : 'bg-gray-200 text-gray-700'}`}
> >
d-Amphetamin {t.dAmphetamine}
</button> </button>
<button <button
onClick={() => updateUiSetting('chartView', 'ldx')} onClick={() => updateUiSetting('chartView', 'ldx')}
className={`px-4 py-2 text-sm font-medium rounded-md ${chartView === 'ldx' ? 'bg-sky-600 text-white' : 'bg-gray-200 text-gray-700'}`} className={`px-4 py-2 text-sm font-medium rounded-md ${chartView === 'ldx' ? 'bg-sky-600 text-white' : 'bg-gray-200 text-gray-700'}`}
> >
Lisdexamfetamin {t.lisdexamfetamine}
</button> </button>
<button <button
onClick={() => updateUiSetting('chartView', 'both')} onClick={() => updateUiSetting('chartView', 'both')}
className={`px-4 py-2 text-sm font-medium rounded-md ${chartView === 'both' ? 'bg-sky-600 text-white' : 'bg-gray-200 text-gray-700'}`} className={`px-4 py-2 text-sm font-medium rounded-md ${chartView === 'both' ? 'bg-sky-600 text-white' : 'bg-gray-200 text-gray-700'}`}
> >
Beide {t.both}
</button> </button>
</div> </div>
<SimulationChart <SimulationChart
idealProfile={idealProfile} idealProfile={idealProfile}
deviatedProfile={deviatedProfile} deviatedProfile={deviatedProfile}
@@ -116,12 +128,13 @@ const MedPlanAssistant = () => {
displayedDays={displayedDays} displayedDays={displayedDays}
yAxisMin={yAxisMin} yAxisMin={yAxisMin}
yAxisMax={yAxisMax} yAxisMax={yAxisMax}
t={t}
/> />
</div> </div>
{/* Right Column - Settings */} {/* Right Column - Settings */}
<div className="lg:col-span-1 space-y-6 lg:order-3"> <div className="lg:col-span-1 space-y-6 lg:order-3">
<Settings <Settings
pkParams={pkParams} pkParams={pkParams}
therapeuticRange={therapeuticRange} therapeuticRange={therapeuticRange}
uiSettings={uiSettings} uiSettings={uiSettings}
@@ -129,17 +142,18 @@ const MedPlanAssistant = () => {
onUpdateTherapeuticRange={(key, value) => updateNestedState('therapeuticRange', key, { [key]: value })} onUpdateTherapeuticRange={(key, value) => updateNestedState('therapeuticRange', key, { [key]: value })}
onUpdateUiSetting={updateUiSetting} onUpdateUiSetting={updateUiSetting}
onReset={handleReset} onReset={handleReset}
t={t}
/> />
</div> </div>
</div> </div>
<footer className="mt-8 p-4 bg-gray-100 rounded-lg text-sm text-gray-700 border"> <footer className="mt-8 p-4 bg-gray-100 rounded-lg text-sm text-gray-700 border">
<h3 className="font-semibold mb-2">Wichtiger Hinweis</h3> <h3 className="font-semibold mb-2">{t.importantNote}</h3>
<p>Dieses Tool dient ausschließlich zu Illustrations- und Informationszwecken. Es ist kein medizinisches Gerät und ersetzt nicht die Beratung durch einen Arzt oder Apotheker. Alle Berechnungen sind Simulationen, die auf allgemeinen pharmakokinetischen Modellen basieren und von individuellen Faktoren erheblich abweichen können. Bitte konsultiere deinen behandelnden Arzt, bevor du Anpassungen an deiner Medikation vornimmst.</p> <p>{t.disclaimer}</p>
</footer> </footer>
</div> </div>
</div> </div>
); );
}; };
export default MedPlanAssistant; export default MedPlanAssistant;

View File

@@ -8,11 +8,12 @@ const DeviationList = ({
simulationDays, simulationDays,
onAddDeviation, onAddDeviation,
onRemoveDeviation, onRemoveDeviation,
onDeviationChange onDeviationChange,
t
}) => { }) => {
return ( return (
<div className="bg-amber-50 p-5 rounded-lg shadow-sm border border-amber-200"> <div className="bg-amber-50 p-5 rounded-lg shadow-sm border border-amber-200">
<h2 className="text-xl font-semibold mb-4 text-gray-700">Abweichungen vom Plan</h2> <h2 className="text-xl font-semibold mb-4 text-gray-700">{t.deviationsFromPlan}</h2>
{deviations.map((dev, index) => ( {deviations.map((dev, index) => (
<div key={index} className="flex items-center space-x-2 mb-2 p-2 bg-white rounded flex-wrap"> <div key={index} className="flex items-center space-x-2 mb-2 p-2 bg-white rounded flex-wrap">
<select <select
@@ -21,7 +22,7 @@ const DeviationList = ({
className="p-2 border rounded-md text-sm" className="p-2 border rounded-md text-sm"
> >
{[...Array(parseInt(simulationDays, 10) || 1).keys()].map(day => ( {[...Array(parseInt(simulationDays, 10) || 1).keys()].map(day => (
<option key={day} value={day}>Tag {day + 1}</option> <option key={day} value={day}>{t.day} {day + 1}</option>
))} ))}
</select> </select>
<TimeInput <TimeInput
@@ -34,7 +35,7 @@ const DeviationList = ({
onChange={newDose => onDeviationChange(index, 'dose', newDose)} onChange={newDose => onDeviationChange(index, 'dose', newDose)}
increment={doseIncrement} increment={doseIncrement}
min={0} min={0}
unit="mg" unit={t.mg}
/> />
</div> </div>
<button <button
@@ -43,7 +44,7 @@ const DeviationList = ({
> >
&times; &times;
</button> </button>
<div className="flex items-center mt-1" title="Mark this if it was an extra dose instead of a replacement for a planned one."> <div className="flex items-center mt-1" title={t.additionalTooltip}>
<input <input
type="checkbox" type="checkbox"
id={`add_dose_${index}`} id={`add_dose_${index}`}
@@ -52,7 +53,7 @@ const DeviationList = ({
className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500" className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
/> />
<label htmlFor={`add_dose_${index}`} className="ml-2 text-xs text-gray-600"> <label htmlFor={`add_dose_${index}`} className="ml-2 text-xs text-gray-600">
Zusätzlich {t.additional}
</label> </label>
</div> </div>
</div> </div>
@@ -61,10 +62,8 @@ const DeviationList = ({
onClick={onAddDeviation} onClick={onAddDeviation}
className="mt-2 w-full bg-amber-500 text-white py-2 rounded-md hover:bg-amber-600 text-sm" className="mt-2 w-full bg-amber-500 text-white py-2 rounded-md hover:bg-amber-600 text-sm"
> >
Abweichung hinzufügen {t.addDeviation}
</button> </button>
</div> </div>
); );
}; };export default DeviationList;
export default DeviationList;

View File

@@ -2,10 +2,10 @@ import React from 'react';
import TimeInput from './TimeInput.js'; import TimeInput from './TimeInput.js';
import NumericInput from './NumericInput.js'; import NumericInput from './NumericInput.js';
const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses }) => { const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses, t }) => {
return ( return (
<div className="bg-white p-5 rounded-lg shadow-sm border"> <div className="bg-white p-5 rounded-lg shadow-sm border">
<h2 className="text-xl font-semibold mb-4 text-gray-700">Mein Plan</h2> <h2 className="text-xl font-semibold mb-4 text-gray-700">{t.myPlan}</h2>
{doses.map((dose, index) => ( {doses.map((dose, index) => (
<div key={index} className="flex items-center space-x-3 mb-3"> <div key={index} className="flex items-center space-x-3 mb-3">
<TimeInput <TimeInput
@@ -18,10 +18,10 @@ const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses }) => {
onChange={newDose => onUpdateDoses(doses.map((d, i) => i === index ? {...d, dose: newDose} : d))} onChange={newDose => onUpdateDoses(doses.map((d, i) => i === index ? {...d, dose: newDose} : d))}
increment={doseIncrement} increment={doseIncrement}
min={0} min={0}
unit="mg" unit={t.mg}
/> />
</div> </div>
<span className="text-gray-600 text-sm flex-1">{dose.label}</span> <span className="text-gray-600 text-sm flex-1">{t[dose.label] || dose.label}</span>
</div> </div>
))} ))}
</div> </div>

View File

@@ -0,0 +1,19 @@
import React from 'react';
const LanguageSelector = ({ currentLanguage, onLanguageChange, t }) => {
return (
<div className="flex items-center space-x-2">
<label className="text-sm font-medium text-gray-600">{t.language}:</label>
<select
value={currentLanguage}
onChange={(e) => onLanguageChange(e.target.value)}
className="p-1 border rounded text-sm bg-white"
>
<option value="en">{t.english}</option>
<option value="de">{t.german}</option>
</select>
</div>
);
};
export default LanguageSelector;

View File

@@ -8,13 +8,14 @@ const Settings = ({
onUpdatePkParams, onUpdatePkParams,
onUpdateTherapeuticRange, onUpdateTherapeuticRange,
onUpdateUiSetting, onUpdateUiSetting,
onReset onReset,
t
}) => { }) => {
const { showDayTimeXAxis, yAxisMin, yAxisMax, simulationDays, displayedDays } = uiSettings; const { showDayTimeXAxis, yAxisMin, yAxisMax, simulationDays, displayedDays } = uiSettings;
return ( return (
<div className="bg-white p-5 rounded-lg shadow-sm border"> <div className="bg-white p-5 rounded-lg shadow-sm border">
<h2 className="text-xl font-semibold mb-4 text-gray-700">Erweiterte Einstellungen</h2> <h2 className="text-xl font-semibold mb-4 text-gray-700">{t.advancedSettings}</h2>
<div className="space-y-4 text-sm"> <div className="space-y-4 text-sm">
<div className="flex items-center"> <div className="flex items-center">
<input <input
@@ -25,11 +26,11 @@ const Settings = ({
className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500" className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
/> />
<label htmlFor="showDayTimeXAxis" className="ml-3 block font-medium text-gray-600"> <label htmlFor="showDayTimeXAxis" className="ml-3 block font-medium text-gray-600">
24h-Zeitachse anzeigen {t.show24hTimeAxis}
</label> </label>
</div> </div>
<label className="block font-medium text-gray-600 pt-2">Simulationsdauer</label> <label className="block font-medium text-gray-600 pt-2">{t.simulationDuration}</label>
<div className="w-40"> <div className="w-40">
<NumericInput <NumericInput
value={simulationDays} value={simulationDays}
@@ -37,11 +38,11 @@ const Settings = ({
increment={'1'} increment={'1'}
min={2} min={2}
max={7} max={7}
unit="Tage" unit={t.days}
/> />
</div> </div>
<label className="block font-medium text-gray-600">Angezeigte Tage</label> <label className="block font-medium text-gray-600">{t.displayedDays}</label>
<div className="w-40"> <div className="w-40">
<NumericInput <NumericInput
value={displayedDays} value={displayedDays}
@@ -49,11 +50,11 @@ const Settings = ({
increment={'1'} increment={'1'}
min={1} min={1}
max={parseInt(simulationDays, 10) || 1} max={parseInt(simulationDays, 10) || 1}
unit="Tage" unit={t.days}
/> />
</div> </div>
<label className="block font-medium text-gray-600 pt-2">Y-Achsen-Bereich</label> <label className="block font-medium text-gray-600 pt-2">{t.yAxisRange}</label>
<div className="flex items-center space-x-2 mt-1"> <div className="flex items-center space-x-2 mt-1">
<div className="w-32"> <div className="w-32">
<NumericInput <NumericInput
@@ -61,7 +62,7 @@ const Settings = ({
onChange={val => onUpdateUiSetting('yAxisMin', val)} onChange={val => onUpdateUiSetting('yAxisMin', val)}
increment={'5'} increment={'5'}
min={0} min={0}
placeholder="Auto" placeholder={t.auto}
unit="ng/ml" unit="ng/ml"
/> />
</div> </div>
@@ -72,13 +73,13 @@ const Settings = ({
onChange={val => onUpdateUiSetting('yAxisMax', val)} onChange={val => onUpdateUiSetting('yAxisMax', val)}
increment={'5'} increment={'5'}
min={0} min={0}
placeholder="Auto" placeholder={t.auto}
unit="ng/ml" unit="ng/ml"
/> />
</div> </div>
</div> </div>
<label className="block font-medium text-gray-600">Therapeutischer Bereich</label> <label className="block font-medium text-gray-600">{t.therapeuticRange}</label>
<div className="flex items-center space-x-2 mt-1"> <div className="flex items-center space-x-2 mt-1">
<div className="w-32"> <div className="w-32">
<NumericInput <NumericInput
@@ -86,7 +87,7 @@ const Settings = ({
onChange={val => onUpdateTherapeuticRange('min', val)} onChange={val => onUpdateTherapeuticRange('min', val)}
increment={'0.5'} increment={'0.5'}
min={0} min={0}
placeholder="Min" placeholder={t.min}
unit="ng/ml" unit="ng/ml"
/> />
</div> </div>
@@ -97,15 +98,15 @@ const Settings = ({
onChange={val => onUpdateTherapeuticRange('max', val)} onChange={val => onUpdateTherapeuticRange('max', val)}
increment={'0.5'} increment={'0.5'}
min={0} min={0}
placeholder="Max" placeholder={t.max}
unit="ng/ml" unit="ng/ml"
/> />
</div> </div>
</div> </div>
<h3 className="text-lg font-semibold mt-4 pt-4 border-t">d-Amphetamin Parameter</h3> <h3 className="text-lg font-semibold mt-4 pt-4 border-t">{t.dAmphetamineParameters}</h3>
<div className="w-40"> <div className="w-40">
<label className="block font-medium text-gray-600">Halbwertszeit</label> <label className="block font-medium text-gray-600">{t.halfLife}</label>
<NumericInput <NumericInput
value={pkParams.damph.halfLife} value={pkParams.damph.halfLife}
onChange={val => onUpdatePkParams('damph', { halfLife: val })} onChange={val => onUpdatePkParams('damph', { halfLife: val })}
@@ -115,9 +116,9 @@ const Settings = ({
/> />
</div> </div>
<h3 className="text-lg font-semibold mt-4 pt-4 border-t">Lisdexamfetamin Parameter</h3> <h3 className="text-lg font-semibold mt-4 pt-4 border-t">{t.lisdexamfetamineParameters}</h3>
<div className="w-40"> <div className="w-40">
<label className="block font-medium text-gray-600">Umwandlungs-Halbwertszeit</label> <label className="block font-medium text-gray-600">{t.conversionHalfLife}</label>
<NumericInput <NumericInput
value={pkParams.ldx.halfLife} value={pkParams.ldx.halfLife}
onChange={val => onUpdatePkParams('ldx', { halfLife: val })} onChange={val => onUpdatePkParams('ldx', { halfLife: val })}
@@ -127,13 +128,13 @@ const Settings = ({
/> />
</div> </div>
<div className="w-40"> <div className="w-40">
<label className="block font-medium text-gray-600">Absorptionsrate</label> <label className="block font-medium text-gray-600">{t.absorptionRate}</label>
<NumericInput <NumericInput
value={pkParams.ldx.absorptionRate} value={pkParams.ldx.absorptionRate}
onChange={val => onUpdatePkParams('ldx', { absorptionRate: val })} onChange={val => onUpdatePkParams('ldx', { absorptionRate: val })}
increment={'0.1'} increment={'0.1'}
min={0.1} min={0.1}
unit="(schneller >)" unit={t.faster}
/> />
</div> </div>
@@ -142,7 +143,7 @@ const Settings = ({
onClick={onReset} onClick={onReset}
className="w-full bg-red-600 text-white py-2 rounded-md hover:bg-red-700 text-sm" className="w-full bg-red-600 text-white py-2 rounded-md hover:bg-red-700 text-sm"
> >
Alle Einstellungen zurücksetzen {t.resetAllSettings}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -11,7 +11,8 @@ const SimulationChart = ({
simulationDays, simulationDays,
displayedDays, displayedDays,
yAxisMin, yAxisMin,
yAxisMax yAxisMax,
t
}) => { }) => {
const totalHours = (parseInt(simulationDays, 10) || 3) * 24; const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
const chartTicks = Array.from({length: Math.floor(totalHours / 6) + 1}, (_, i) => i * 6); const chartTicks = Array.from({length: Math.floor(totalHours / 6) + 1}, (_, i) => i * 6);
@@ -36,7 +37,7 @@ const SimulationChart = ({
type="number" type="number"
domain={[0, totalHours]} domain={[0, totalHours]}
ticks={chartTicks} ticks={chartTicks}
tickFormatter={(h) => `${h}h`} tickFormatter={(h) => `${h}${t.hour}`}
xAxisId="continuous" xAxisId="continuous"
/> />
{showDayTimeXAxis && ( {showDayTimeXAxis && (
@@ -45,26 +46,26 @@ const SimulationChart = ({
type="number" type="number"
domain={[0, totalHours]} domain={[0, totalHours]}
ticks={chartTicks} ticks={chartTicks}
tickFormatter={(h) => `${h % 24}h`} tickFormatter={(h) => `${h % 24}${t.hour}`}
xAxisId="daytime" xAxisId="daytime"
orientation="top" orientation="top"
/> />
)} )}
<YAxis <YAxis
label={{ value: 'Konzentration (ng/ml)', angle: -90, position: 'insideLeft', offset: -10 }} label={{ value: t.concentration, angle: -90, position: 'insideLeft', offset: -10 }}
domain={chartDomain} domain={chartDomain}
allowDecimals={false} allowDecimals={false}
/> />
<Tooltip <Tooltip
formatter={(value, name) => [`${value.toFixed(1)} ng/ml`, name]} formatter={(value, name) => [`${value.toFixed(1)} ${t.ngml}`, name]}
labelFormatter={(label) => `Stunde: ${label}h`} labelFormatter={(label) => `${t.hour.replace('h', 'Hour')}: ${label}${t.hour}`}
/> />
<Legend verticalAlign="top" height={36} /> <Legend verticalAlign="top" height={36} />
{(chartView === 'damph' || chartView === 'both') && ( {(chartView === 'damph' || chartView === 'both') && (
<ReferenceLine <ReferenceLine
y={parseFloat(therapeuticRange.min) || 0} y={parseFloat(therapeuticRange.min) || 0}
label={{ value: 'Min', position: 'insideTopLeft' }} label={{ value: t.min, position: 'insideTopLeft' }}
stroke="green" stroke="green"
strokeDasharray="3 3" strokeDasharray="3 3"
xAxisId="continuous" xAxisId="continuous"
@@ -73,7 +74,7 @@ const SimulationChart = ({
{(chartView === 'damph' || chartView === 'both') && ( {(chartView === 'damph' || chartView === 'both') && (
<ReferenceLine <ReferenceLine
y={parseFloat(therapeuticRange.max) || 0} y={parseFloat(therapeuticRange.max) || 0}
label={{ value: 'Max', position: 'insideTopLeft' }} label={{ value: t.max, position: 'insideTopLeft' }}
stroke="red" stroke="red"
strokeDasharray="3 3" strokeDasharray="3 3"
xAxisId="continuous" xAxisId="continuous"
@@ -97,7 +98,7 @@ const SimulationChart = ({
type="monotone" type="monotone"
data={idealProfile} data={idealProfile}
dataKey="damph" dataKey="damph"
name="d-Amphetamin (Ideal)" name={`${t.dAmphetamine} (Ideal)`}
stroke="#3b82f6" stroke="#3b82f6"
strokeWidth={2.5} strokeWidth={2.5}
dot={false} dot={false}
@@ -109,7 +110,7 @@ const SimulationChart = ({
type="monotone" type="monotone"
data={idealProfile} data={idealProfile}
dataKey="ldx" dataKey="ldx"
name="Lisdexamfetamin (Ideal)" name={`${t.lisdexamfetamine} (Ideal)`}
stroke="#8b5cf6" stroke="#8b5cf6"
strokeWidth={2} strokeWidth={2}
dot={false} dot={false}
@@ -123,7 +124,7 @@ const SimulationChart = ({
type="monotone" type="monotone"
data={deviatedProfile} data={deviatedProfile}
dataKey="damph" dataKey="damph"
name="d-Amphetamin (Abweichung)" name={`${t.dAmphetamine} (Deviation)`}
stroke="#f59e0b" stroke="#f59e0b"
strokeWidth={2} strokeWidth={2}
strokeDasharray="5 5" strokeDasharray="5 5"
@@ -136,7 +137,7 @@ const SimulationChart = ({
type="monotone" type="monotone"
data={deviatedProfile} data={deviatedProfile}
dataKey="ldx" dataKey="ldx"
name="Lisdexamfetamin (Abweichung)" name={`${t.lisdexamfetamine} (Deviation)`}
stroke="#f97316" stroke="#f97316"
strokeWidth={1.5} strokeWidth={1.5}
strokeDasharray="5 5" strokeDasharray="5 5"
@@ -150,7 +151,7 @@ const SimulationChart = ({
type="monotone" type="monotone"
data={correctedProfile} data={correctedProfile}
dataKey="damph" dataKey="damph"
name="d-Amphetamin (Korrektur)" name={`${t.dAmphetamine} (Correction)`}
stroke="#10b981" stroke="#10b981"
strokeWidth={2.5} strokeWidth={2.5}
strokeDasharray="3 7" strokeDasharray="3 7"
@@ -163,7 +164,7 @@ const SimulationChart = ({
type="monotone" type="monotone"
data={correctedProfile} data={correctedProfile}
dataKey="ldx" dataKey="ldx"
name="Lisdexamfetamin (Korrektur)" name={`${t.lisdexamfetamine} (Correction)`}
stroke="#059669" stroke="#059669"
strokeWidth={2} strokeWidth={2}
strokeDasharray="3 7" strokeDasharray="3 7"
@@ -176,6 +177,4 @@ const SimulationChart = ({
</div> </div>
</div> </div>
); );
}; };export default SimulationChart;
export default SimulationChart;

View File

@@ -1,21 +1,21 @@
import React from 'react'; import React from 'react';
const SuggestionPanel = ({ suggestion, onApplySuggestion }) => { const SuggestionPanel = ({ suggestion, onApplySuggestion, t }) => {
if (!suggestion) return null; if (!suggestion) return null;
return ( return (
<div className="bg-sky-100 border-l-4 border-sky-500 p-4 rounded-r-lg shadow-md"> <div className="bg-sky-100 border-l-4 border-sky-500 p-4 rounded-r-lg shadow-md">
<h3 className="font-bold text-lg mb-2">Was wäre wenn?</h3> <h3 className="font-bold text-lg mb-2">{t.whatIf}</h3>
{suggestion.dose ? ( {suggestion.dose ? (
<> <>
<p className="text-sm text-sky-800 mb-3"> <p className="text-sm text-sky-800 mb-3">
Vorschlag: <span className="font-bold">{suggestion.dose}mg</span> (statt {suggestion.originalDose}mg) um <span className="font-bold">{suggestion.time}</span>. {t.suggestion}: <span className="font-bold">{suggestion.dose}{t.mg}</span> ({t.instead} {suggestion.originalDose}{t.mg}) {t.at} <span className="font-bold">{suggestion.time}</span>.
</p> </p>
<button <button
onClick={onApplySuggestion} onClick={onApplySuggestion}
className="w-full bg-sky-600 text-white py-2 rounded-md hover:bg-sky-700 text-sm" className="w-full bg-sky-600 text-white py-2 rounded-md hover:bg-sky-700 text-sm"
> >
Vorschlag als Abweichung übernehmen {t.applySuggestion}
</button> </button>
</> </>
) : ( ) : (
@@ -23,6 +23,4 @@ const SuggestionPanel = ({ suggestion, onApplySuggestion }) => {
)} )}
</div> </div>
); );
}; };export default SuggestionPanel;
export default SuggestionPanel;

View File

@@ -67,7 +67,7 @@ const TimeInput = ({ value, onChange }) => {
<div className="absolute top-full mt-2 z-10 bg-white p-4 rounded-lg shadow-xl border w-64"> <div className="absolute top-full mt-2 z-10 bg-white p-4 rounded-lg shadow-xl border w-64">
<div className="text-center text-lg font-bold mb-3">{value}</div> <div className="text-center text-lg font-bold mb-3">{value}</div>
<div> <div>
<div className="mb-2"><span className="font-semibold">Stunde:</span></div> <div className="mb-2"><span className="font-semibold">Hour:</span></div>
<div className="grid grid-cols-6 gap-1"> <div className="grid grid-cols-6 gap-1">
{[...Array(24).keys()].map(h => ( {[...Array(24).keys()].map(h => (
<button <button
@@ -98,7 +98,7 @@ const TimeInput = ({ value, onChange }) => {
onClick={() => setIsPickerOpen(false)} onClick={() => setIsPickerOpen(false)}
className="mt-4 w-full bg-gray-600 text-white py-1 rounded-md text-sm" className="mt-4 w-full bg-gray-600 text-white py-1 rounded-md text-sm"
> >
Schließen Close
</button> </button>
</div> </div>
)} )}

View File

@@ -1,19 +1,19 @@
// --- Constants --- // Application constants
export const LOCAL_STORAGE_KEY = 'medPlanAssistantState_v5'; export const LOCAL_STORAGE_KEY = 'medPlanAssistantState_v5';
export const LDX_TO_DAMPH_CONVERSION_FACTOR = 0.2948; export const LDX_TO_DAMPH_CONVERSION_FACTOR = 0.2948;
// --- Default State --- // Default application state
export const getDefaultState = () => ({ export const getDefaultState = () => ({
pkParams: { pkParams: {
damph: { halfLife: '11' }, damph: { halfLife: '11' },
ldx: { halfLife: '0.8', absorptionRate: '1.5' }, ldx: { halfLife: '0.8', absorptionRate: '1.5' },
}, },
doses: [ doses: [
{ time: '06:30', dose: '25', label: 'Morgens' }, { time: '06:30', dose: '25', label: 'morning' },
{ time: '12:30', dose: '10', label: 'Mittags' }, { time: '12:30', dose: '10', label: 'midday' },
{ time: '17:00', dose: '10', label: 'Nachmittags' }, { time: '17:00', dose: '10', label: 'afternoon' },
{ time: '21:00', dose: '10', label: 'Abends' }, { time: '21:00', dose: '10', label: 'evening' },
{ time: '01:00', dose: '0', label: 'Nachts' }, { time: '01:00', dose: '0', label: 'night' },
], ],
steadyStateConfig: { daysOnMedication: '7' }, steadyStateConfig: { daysOnMedication: '7' },
therapeuticRange: { min: '11.5', max: '14' }, therapeuticRange: { min: '11.5', max: '14' },

24
src/hooks/useLanguage.js Normal file
View File

@@ -0,0 +1,24 @@
import React from 'react';
import { translations, getInitialLanguage } from '../locales/index.js';
export const useLanguage = () => {
const [currentLanguage, setCurrentLanguage] = React.useState(getInitialLanguage);
// Get current translations
const t = translations[currentLanguage] || translations.en;
// Change language and save to localStorage
const changeLanguage = (lang) => {
if (translations[lang]) {
setCurrentLanguage(lang);
localStorage.setItem('medPlanAssistant_language', lang);
}
};
return {
currentLanguage,
changeLanguage,
t,
availableLanguages: Object.keys(translations)
};
};

77
src/locales/de.js Normal file
View File

@@ -0,0 +1,77 @@
// German translations
export const de = {
// Header
appTitle: "Medikationsplan-Assistent",
appSubtitle: "Simulation für Lisdexamfetamin (LDX) und d-Amphetamin (d-amph)",
// Chart view buttons
dAmphetamine: "d-Amphetamin",
lisdexamfetamine: "Lisdexamfetamin",
both: "Beide",
// Language selector
language: "Sprache",
english: "English",
german: "Deutsch",
// Dose Schedule
myPlan: "Mein Plan",
morning: "Morgens",
midday: "Mittags",
afternoon: "Nachmittags",
evening: "Abends",
night: "Nachts",
// Deviations
deviationsFromPlan: "Abweichungen vom Plan",
addDeviation: "Abweichung hinzufügen",
day: "Tag",
additional: "Zusätzlich",
additionalTooltip: "Markiere dies, wenn es eine zusätzliche Dosis war anstatt eines Ersatzes für eine geplante.",
// Suggestions
whatIf: "Was wäre wenn?",
suggestion: "Vorschlag",
instead: "statt",
at: "um",
applySuggestion: "Vorschlag als Abweichung übernehmen",
noSignificantCorrection: "Keine signifikante Korrektur notwendig.",
noSuitableNextDose: "Keine passende nächste Dosis für Korrektur gefunden.",
// Chart
concentration: "Konzentration (ng/ml)",
hour: "h",
min: "Min",
max: "Max",
// Settings
advancedSettings: "Erweiterte Einstellungen",
show24hTimeAxis: "24h-Zeitachse anzeigen",
simulationDuration: "Simulationsdauer",
days: "Tage",
displayedDays: "Angezeigte Tage",
yAxisRange: "Y-Achsen-Bereich",
auto: "Auto",
therapeuticRange: "Therapeutischer Bereich",
dAmphetamineParameters: "d-Amphetamin Parameter",
halfLife: "Halbwertszeit",
hours: "h",
lisdexamfetamineParameters: "Lisdexamfetamin Parameter",
conversionHalfLife: "Umwandlungs-Halbwertszeit",
absorptionRate: "Absorptionsrate",
faster: "(schneller >)",
resetAllSettings: "Alle Einstellungen zurücksetzen",
// Units
mg: "mg",
ngml: "ng/ml",
// Reset confirmation
resetConfirmation: "Bist du sicher, dass du alle Einstellungen auf die Standardwerte zurücksetzen möchtest? Dies kann nicht rückgängig gemacht werden.",
// Footer disclaimer
importantNote: "Wichtiger Hinweis",
disclaimer: "Dieses Tool dient ausschließlich zu Illustrations- und Informationszwecken. Es ist kein medizinisches Gerät und ersetzt nicht die Beratung durch einen Arzt oder Apotheker. Alle Berechnungen sind Simulationen, die auf allgemeinen pharmakokinetischen Modellen basieren und von individuellen Faktoren erheblich abweichen können. Bitte konsultiere deinen behandelnden Arzt, bevor du Anpassungen an deiner Medikation vornimmst."
};
export default de;

77
src/locales/en.js Normal file
View File

@@ -0,0 +1,77 @@
// English translations
export const en = {
// App header and navigation
appTitle: "Medication Plan Assistant",
appSubtitle: "Simulation for Lisdexamfetamine (LDX) and d-Amphetamine (d-amph)",
// Chart view buttons
dAmphetamine: "d-Amphetamine",
lisdexamfetamine: "Lisdexamfetamine",
both: "Both",
// Language selector
language: "Language",
english: "English",
german: "Deutsch",
// Dose Schedule
myPlan: "My Plan",
morning: "Morning",
midday: "Midday",
afternoon: "Afternoon",
evening: "Evening",
night: "Night",
// Deviations
deviationsFromPlan: "Deviations from Plan",
addDeviation: "Add Deviation",
day: "Day",
additional: "Additional",
additionalTooltip: "Mark this if it was an extra dose instead of a replacement for a planned one.",
// Suggestions
whatIf: "What if?",
suggestion: "Suggestion",
instead: "instead",
at: "at",
applySuggestion: "Apply suggestion as deviation",
noSignificantCorrection: "No significant correction necessary.",
noSuitableNextDose: "No suitable next dose found for correction.",
// Chart
concentration: "Concentration (ng/ml)",
hour: "h",
min: "Min",
max: "Max",
// Settings
advancedSettings: "Advanced Settings",
show24hTimeAxis: "Show 24h time axis",
simulationDuration: "Simulation Duration",
days: "Days",
displayedDays: "Displayed Days",
yAxisRange: "Y-Axis Range",
auto: "Auto",
therapeuticRange: "Therapeutic Range",
dAmphetamineParameters: "d-Amphetamine Parameters",
halfLife: "Half-life",
hours: "h",
lisdexamfetamineParameters: "Lisdexamfetamine Parameters",
conversionHalfLife: "Conversion Half-life",
absorptionRate: "Absorption Rate",
faster: "(faster >)",
resetAllSettings: "Reset All Settings",
// Units
mg: "mg",
ngml: "ng/ml",
// Reset confirmation
resetConfirmation: "Are you sure you want to reset all settings to default values? This cannot be undone.",
// Footer disclaimer
importantNote: "Important Notice",
disclaimer: "This tool is for illustration and information purposes only. It is not a medical device and does not replace consultation with a doctor or pharmacist. All calculations are simulations based on general pharmacokinetic models and may differ significantly from individual factors. Please consult your treating physician before making adjustments to your medication."
};
export default en;

24
src/locales/index.js Normal file
View File

@@ -0,0 +1,24 @@
import en from './en.js';
import de from './de.js';
export const translations = {
en,
de
};
// Get browser language preference
export const getBrowserLanguage = () => {
const browserLang = navigator.language || navigator.userLanguage;
return browserLang.startsWith('de') ? 'de' : 'en';
};
// Get stored language or fall back to browser preference or English
export const getInitialLanguage = () => {
const stored = localStorage.getItem('medPlanAssistant_language');
if (stored && translations[stored]) {
return stored;
}
return getBrowserLanguage();
};
export default translations;