Add localization support and docs
This commit is contained in:
100
src/App.js
100
src/App.js
@@ -6,36 +6,40 @@ import DeviationList from './components/DeviationList.js';
|
||||
import SuggestionPanel from './components/SuggestionPanel.js';
|
||||
import SimulationChart from './components/SimulationChart.js';
|
||||
import Settings from './components/Settings.js';
|
||||
import LanguageSelector from './components/LanguageSelector.js';
|
||||
|
||||
// Custom Hooks
|
||||
import { useAppState } from './hooks/useAppState.js';
|
||||
import { useSimulation } from './hooks/useSimulation.js';
|
||||
import { useLanguage } from './hooks/useLanguage.js';
|
||||
|
||||
// --- Main Component ---
|
||||
const MedPlanAssistant = () => {
|
||||
const {
|
||||
appState,
|
||||
updateState,
|
||||
updateNestedState,
|
||||
updateUiSetting,
|
||||
handleReset
|
||||
const { currentLanguage, t, changeLanguage } = useLanguage();
|
||||
|
||||
const {
|
||||
appState,
|
||||
updateState,
|
||||
updateNestedState,
|
||||
updateUiSetting,
|
||||
handleReset
|
||||
} = useAppState();
|
||||
|
||||
const {
|
||||
pkParams,
|
||||
doses,
|
||||
therapeuticRange,
|
||||
doseIncrement,
|
||||
uiSettings
|
||||
const {
|
||||
pkParams,
|
||||
doses,
|
||||
therapeuticRange,
|
||||
doseIncrement,
|
||||
uiSettings
|
||||
} = appState;
|
||||
|
||||
const {
|
||||
showDayTimeXAxis,
|
||||
chartView,
|
||||
yAxisMin,
|
||||
yAxisMax,
|
||||
simulationDays,
|
||||
displayedDays
|
||||
|
||||
const {
|
||||
showDayTimeXAxis,
|
||||
chartView,
|
||||
yAxisMin,
|
||||
yAxisMax,
|
||||
simulationDays,
|
||||
displayedDays
|
||||
} = uiSettings;
|
||||
|
||||
const {
|
||||
@@ -54,57 +58,65 @@ const MedPlanAssistant = () => {
|
||||
<div className="bg-gray-100 font-sans p-4 sm:p-6 lg:p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-800">Medikationsplan-Assistent</h1>
|
||||
<p className="text-gray-600 mt-1">Simulation für Lisdexamfetamin (LDX) und d-Amphetamin (d-amph)</p>
|
||||
<div className="flex justify-between items-start">
|
||||
<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>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left Column - Controls */}
|
||||
<div className="lg:col-span-1 space-y-6 lg:order-1">
|
||||
<DoseSchedule
|
||||
<DoseSchedule
|
||||
doses={doses}
|
||||
doseIncrement={doseIncrement}
|
||||
onUpdateDoses={(newDoses) => updateState('doses', newDoses)}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<DeviationList
|
||||
<DeviationList
|
||||
deviations={deviations}
|
||||
doseIncrement={doseIncrement}
|
||||
simulationDays={simulationDays}
|
||||
onAddDeviation={addDeviation}
|
||||
onRemoveDeviation={removeDeviation}
|
||||
onDeviationChange={handleDeviationChange}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<SuggestionPanel
|
||||
<SuggestionPanel
|
||||
suggestion={suggestion}
|
||||
onApplySuggestion={applySuggestion}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 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="flex justify-center space-x-2 mb-4">
|
||||
<button
|
||||
onClick={() => updateUiSetting('chartView', 'damph')}
|
||||
<button
|
||||
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'}`}
|
||||
>
|
||||
d-Amphetamin
|
||||
{t.dAmphetamine}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateUiSetting('chartView', 'ldx')}
|
||||
<button
|
||||
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'}`}
|
||||
>
|
||||
Lisdexamfetamin
|
||||
{t.lisdexamfetamine}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => updateUiSetting('chartView', 'both')}
|
||||
<button
|
||||
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'}`}
|
||||
>
|
||||
Beide
|
||||
{t.both}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<SimulationChart
|
||||
idealProfile={idealProfile}
|
||||
deviatedProfile={deviatedProfile}
|
||||
@@ -116,12 +128,13 @@ const MedPlanAssistant = () => {
|
||||
displayedDays={displayedDays}
|
||||
yAxisMin={yAxisMin}
|
||||
yAxisMax={yAxisMax}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Right Column - Settings */}
|
||||
<div className="lg:col-span-1 space-y-6 lg:order-3">
|
||||
<Settings
|
||||
<Settings
|
||||
pkParams={pkParams}
|
||||
therapeuticRange={therapeuticRange}
|
||||
uiSettings={uiSettings}
|
||||
@@ -129,17 +142,18 @@ const MedPlanAssistant = () => {
|
||||
onUpdateTherapeuticRange={(key, value) => updateNestedState('therapeuticRange', key, { [key]: value })}
|
||||
onUpdateUiSetting={updateUiSetting}
|
||||
onReset={handleReset}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<h3 className="font-semibold mb-2">{t.importantNote}</h3>
|
||||
<p>{t.disclaimer}</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MedPlanAssistant;
|
||||
export default MedPlanAssistant;
|
||||
|
||||
@@ -8,11 +8,12 @@ const DeviationList = ({
|
||||
simulationDays,
|
||||
onAddDeviation,
|
||||
onRemoveDeviation,
|
||||
onDeviationChange
|
||||
onDeviationChange,
|
||||
t
|
||||
}) => {
|
||||
return (
|
||||
<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) => (
|
||||
<div key={index} className="flex items-center space-x-2 mb-2 p-2 bg-white rounded flex-wrap">
|
||||
<select
|
||||
@@ -21,7 +22,7 @@ const DeviationList = ({
|
||||
className="p-2 border rounded-md text-sm"
|
||||
>
|
||||
{[...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>
|
||||
<TimeInput
|
||||
@@ -34,7 +35,7 @@ const DeviationList = ({
|
||||
onChange={newDose => onDeviationChange(index, 'dose', newDose)}
|
||||
increment={doseIncrement}
|
||||
min={0}
|
||||
unit="mg"
|
||||
unit={t.mg}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@@ -43,7 +44,7 @@ const DeviationList = ({
|
||||
>
|
||||
×
|
||||
</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
|
||||
type="checkbox"
|
||||
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"
|
||||
/>
|
||||
<label htmlFor={`add_dose_${index}`} className="ml-2 text-xs text-gray-600">
|
||||
Zusätzlich
|
||||
{t.additional}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,10 +62,8 @@ const DeviationList = ({
|
||||
onClick={onAddDeviation}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviationList;
|
||||
};export default DeviationList;
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react';
|
||||
import TimeInput from './TimeInput.js';
|
||||
import NumericInput from './NumericInput.js';
|
||||
|
||||
const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses }) => {
|
||||
const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses, t }) => {
|
||||
return (
|
||||
<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) => (
|
||||
<div key={index} className="flex items-center space-x-3 mb-3">
|
||||
<TimeInput
|
||||
@@ -18,10 +18,10 @@ const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses }) => {
|
||||
onChange={newDose => onUpdateDoses(doses.map((d, i) => i === index ? {...d, dose: newDose} : d))}
|
||||
increment={doseIncrement}
|
||||
min={0}
|
||||
unit="mg"
|
||||
unit={t.mg}
|
||||
/>
|
||||
</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>
|
||||
|
||||
19
src/components/LanguageSelector.js
Normal file
19
src/components/LanguageSelector.js
Normal 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;
|
||||
@@ -8,13 +8,14 @@ const Settings = ({
|
||||
onUpdatePkParams,
|
||||
onUpdateTherapeuticRange,
|
||||
onUpdateUiSetting,
|
||||
onReset
|
||||
onReset,
|
||||
t
|
||||
}) => {
|
||||
const { showDayTimeXAxis, yAxisMin, yAxisMax, simulationDays, displayedDays } = uiSettings;
|
||||
|
||||
return (
|
||||
<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="flex items-center">
|
||||
<input
|
||||
@@ -25,11 +26,11 @@ const Settings = ({
|
||||
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">
|
||||
24h-Zeitachse anzeigen
|
||||
{t.show24hTimeAxis}
|
||||
</label>
|
||||
</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">
|
||||
<NumericInput
|
||||
value={simulationDays}
|
||||
@@ -37,11 +38,11 @@ const Settings = ({
|
||||
increment={'1'}
|
||||
min={2}
|
||||
max={7}
|
||||
unit="Tage"
|
||||
unit={t.days}
|
||||
/>
|
||||
</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">
|
||||
<NumericInput
|
||||
value={displayedDays}
|
||||
@@ -49,11 +50,11 @@ const Settings = ({
|
||||
increment={'1'}
|
||||
min={1}
|
||||
max={parseInt(simulationDays, 10) || 1}
|
||||
unit="Tage"
|
||||
unit={t.days}
|
||||
/>
|
||||
</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="w-32">
|
||||
<NumericInput
|
||||
@@ -61,7 +62,7 @@ const Settings = ({
|
||||
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
||||
increment={'5'}
|
||||
min={0}
|
||||
placeholder="Auto"
|
||||
placeholder={t.auto}
|
||||
unit="ng/ml"
|
||||
/>
|
||||
</div>
|
||||
@@ -72,13 +73,13 @@ const Settings = ({
|
||||
onChange={val => onUpdateUiSetting('yAxisMax', val)}
|
||||
increment={'5'}
|
||||
min={0}
|
||||
placeholder="Auto"
|
||||
placeholder={t.auto}
|
||||
unit="ng/ml"
|
||||
/>
|
||||
</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="w-32">
|
||||
<NumericInput
|
||||
@@ -86,7 +87,7 @@ const Settings = ({
|
||||
onChange={val => onUpdateTherapeuticRange('min', val)}
|
||||
increment={'0.5'}
|
||||
min={0}
|
||||
placeholder="Min"
|
||||
placeholder={t.min}
|
||||
unit="ng/ml"
|
||||
/>
|
||||
</div>
|
||||
@@ -97,15 +98,15 @@ const Settings = ({
|
||||
onChange={val => onUpdateTherapeuticRange('max', val)}
|
||||
increment={'0.5'}
|
||||
min={0}
|
||||
placeholder="Max"
|
||||
placeholder={t.max}
|
||||
unit="ng/ml"
|
||||
/>
|
||||
</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">
|
||||
<label className="block font-medium text-gray-600">Halbwertszeit</label>
|
||||
<label className="block font-medium text-gray-600">{t.halfLife}</label>
|
||||
<NumericInput
|
||||
value={pkParams.damph.halfLife}
|
||||
onChange={val => onUpdatePkParams('damph', { halfLife: val })}
|
||||
@@ -115,9 +116,9 @@ const Settings = ({
|
||||
/>
|
||||
</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">
|
||||
<label className="block font-medium text-gray-600">Umwandlungs-Halbwertszeit</label>
|
||||
<label className="block font-medium text-gray-600">{t.conversionHalfLife}</label>
|
||||
<NumericInput
|
||||
value={pkParams.ldx.halfLife}
|
||||
onChange={val => onUpdatePkParams('ldx', { halfLife: val })}
|
||||
@@ -127,13 +128,13 @@ const Settings = ({
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
value={pkParams.ldx.absorptionRate}
|
||||
onChange={val => onUpdatePkParams('ldx', { absorptionRate: val })}
|
||||
increment={'0.1'}
|
||||
min={0.1}
|
||||
unit="(schneller >)"
|
||||
unit={t.faster}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -142,7 +143,7 @@ const Settings = ({
|
||||
onClick={onReset}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,8 @@ const SimulationChart = ({
|
||||
simulationDays,
|
||||
displayedDays,
|
||||
yAxisMin,
|
||||
yAxisMax
|
||||
yAxisMax,
|
||||
t
|
||||
}) => {
|
||||
const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
|
||||
const chartTicks = Array.from({length: Math.floor(totalHours / 6) + 1}, (_, i) => i * 6);
|
||||
@@ -36,7 +37,7 @@ const SimulationChart = ({
|
||||
type="number"
|
||||
domain={[0, totalHours]}
|
||||
ticks={chartTicks}
|
||||
tickFormatter={(h) => `${h}h`}
|
||||
tickFormatter={(h) => `${h}${t.hour}`}
|
||||
xAxisId="continuous"
|
||||
/>
|
||||
{showDayTimeXAxis && (
|
||||
@@ -45,26 +46,26 @@ const SimulationChart = ({
|
||||
type="number"
|
||||
domain={[0, totalHours]}
|
||||
ticks={chartTicks}
|
||||
tickFormatter={(h) => `${h % 24}h`}
|
||||
tickFormatter={(h) => `${h % 24}${t.hour}`}
|
||||
xAxisId="daytime"
|
||||
orientation="top"
|
||||
/>
|
||||
)}
|
||||
<YAxis
|
||||
label={{ value: 'Konzentration (ng/ml)', angle: -90, position: 'insideLeft', offset: -10 }}
|
||||
label={{ value: t.concentration, angle: -90, position: 'insideLeft', offset: -10 }}
|
||||
domain={chartDomain}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value, name) => [`${value.toFixed(1)} ng/ml`, name]}
|
||||
labelFormatter={(label) => `Stunde: ${label}h`}
|
||||
formatter={(value, name) => [`${value.toFixed(1)} ${t.ngml}`, name]}
|
||||
labelFormatter={(label) => `${t.hour.replace('h', 'Hour')}: ${label}${t.hour}`}
|
||||
/>
|
||||
<Legend verticalAlign="top" height={36} />
|
||||
|
||||
{(chartView === 'damph' || chartView === 'both') && (
|
||||
<ReferenceLine
|
||||
y={parseFloat(therapeuticRange.min) || 0}
|
||||
label={{ value: 'Min', position: 'insideTopLeft' }}
|
||||
label={{ value: t.min, position: 'insideTopLeft' }}
|
||||
stroke="green"
|
||||
strokeDasharray="3 3"
|
||||
xAxisId="continuous"
|
||||
@@ -73,7 +74,7 @@ const SimulationChart = ({
|
||||
{(chartView === 'damph' || chartView === 'both') && (
|
||||
<ReferenceLine
|
||||
y={parseFloat(therapeuticRange.max) || 0}
|
||||
label={{ value: 'Max', position: 'insideTopLeft' }}
|
||||
label={{ value: t.max, position: 'insideTopLeft' }}
|
||||
stroke="red"
|
||||
strokeDasharray="3 3"
|
||||
xAxisId="continuous"
|
||||
@@ -97,7 +98,7 @@ const SimulationChart = ({
|
||||
type="monotone"
|
||||
data={idealProfile}
|
||||
dataKey="damph"
|
||||
name="d-Amphetamin (Ideal)"
|
||||
name={`${t.dAmphetamine} (Ideal)`}
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
@@ -109,7 +110,7 @@ const SimulationChart = ({
|
||||
type="monotone"
|
||||
data={idealProfile}
|
||||
dataKey="ldx"
|
||||
name="Lisdexamfetamin (Ideal)"
|
||||
name={`${t.lisdexamfetamine} (Ideal)`}
|
||||
stroke="#8b5cf6"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
@@ -123,7 +124,7 @@ const SimulationChart = ({
|
||||
type="monotone"
|
||||
data={deviatedProfile}
|
||||
dataKey="damph"
|
||||
name="d-Amphetamin (Abweichung)"
|
||||
name={`${t.dAmphetamine} (Deviation)`}
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5 5"
|
||||
@@ -136,7 +137,7 @@ const SimulationChart = ({
|
||||
type="monotone"
|
||||
data={deviatedProfile}
|
||||
dataKey="ldx"
|
||||
name="Lisdexamfetamin (Abweichung)"
|
||||
name={`${t.lisdexamfetamine} (Deviation)`}
|
||||
stroke="#f97316"
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray="5 5"
|
||||
@@ -150,7 +151,7 @@ const SimulationChart = ({
|
||||
type="monotone"
|
||||
data={correctedProfile}
|
||||
dataKey="damph"
|
||||
name="d-Amphetamin (Korrektur)"
|
||||
name={`${t.dAmphetamine} (Correction)`}
|
||||
stroke="#10b981"
|
||||
strokeWidth={2.5}
|
||||
strokeDasharray="3 7"
|
||||
@@ -163,7 +164,7 @@ const SimulationChart = ({
|
||||
type="monotone"
|
||||
data={correctedProfile}
|
||||
dataKey="ldx"
|
||||
name="Lisdexamfetamin (Korrektur)"
|
||||
name={`${t.lisdexamfetamine} (Correction)`}
|
||||
stroke="#059669"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="3 7"
|
||||
@@ -176,6 +177,4 @@ const SimulationChart = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimulationChart;
|
||||
};export default SimulationChart;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
const SuggestionPanel = ({ suggestion, onApplySuggestion }) => {
|
||||
const SuggestionPanel = ({ suggestion, onApplySuggestion, t }) => {
|
||||
if (!suggestion) return null;
|
||||
|
||||
return (
|
||||
<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 ? (
|
||||
<>
|
||||
<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>
|
||||
<button
|
||||
onClick={onApplySuggestion}
|
||||
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>
|
||||
</>
|
||||
) : (
|
||||
@@ -23,6 +23,4 @@ const SuggestionPanel = ({ suggestion, onApplySuggestion }) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestionPanel;
|
||||
};export default SuggestionPanel;
|
||||
|
||||
@@ -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="text-center text-lg font-bold mb-3">{value}</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">
|
||||
{[...Array(24).keys()].map(h => (
|
||||
<button
|
||||
@@ -98,7 +98,7 @@ const TimeInput = ({ value, onChange }) => {
|
||||
onClick={() => setIsPickerOpen(false)}
|
||||
className="mt-4 w-full bg-gray-600 text-white py-1 rounded-md text-sm"
|
||||
>
|
||||
Schließen
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// --- Constants ---
|
||||
// Application constants
|
||||
export const LOCAL_STORAGE_KEY = 'medPlanAssistantState_v5';
|
||||
export const LDX_TO_DAMPH_CONVERSION_FACTOR = 0.2948;
|
||||
|
||||
// --- Default State ---
|
||||
// Default application state
|
||||
export const getDefaultState = () => ({
|
||||
pkParams: {
|
||||
damph: { halfLife: '11' },
|
||||
ldx: { halfLife: '0.8', absorptionRate: '1.5' },
|
||||
},
|
||||
doses: [
|
||||
{ time: '06:30', dose: '25', label: 'Morgens' },
|
||||
{ time: '12:30', dose: '10', label: 'Mittags' },
|
||||
{ time: '17:00', dose: '10', label: 'Nachmittags' },
|
||||
{ time: '21:00', dose: '10', label: 'Abends' },
|
||||
{ time: '01:00', dose: '0', label: 'Nachts' },
|
||||
{ time: '06:30', dose: '25', label: 'morning' },
|
||||
{ time: '12:30', dose: '10', label: 'midday' },
|
||||
{ time: '17:00', dose: '10', label: 'afternoon' },
|
||||
{ time: '21:00', dose: '10', label: 'evening' },
|
||||
{ time: '01:00', dose: '0', label: 'night' },
|
||||
],
|
||||
steadyStateConfig: { daysOnMedication: '7' },
|
||||
therapeuticRange: { min: '11.5', max: '14' },
|
||||
|
||||
24
src/hooks/useLanguage.js
Normal file
24
src/hooks/useLanguage.js
Normal 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
77
src/locales/de.js
Normal 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
77
src/locales/en.js
Normal 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
24
src/locales/index.js
Normal 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;
|
||||
Reference in New Issue
Block a user