Update App.js, new modular structure

This commit is contained in:
2025-10-18 21:19:17 +02:00
parent b666b35fed
commit 0fb168b050
16 changed files with 1118 additions and 453 deletions

View File

@@ -0,0 +1,70 @@
import React from 'react';
import TimeInput from './TimeInput.js';
import NumericInput from './NumericInput.js';
const DeviationList = ({
deviations,
doseIncrement,
simulationDays,
onAddDeviation,
onRemoveDeviation,
onDeviationChange
}) => {
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>
{deviations.map((dev, index) => (
<div key={index} className="flex items-center space-x-2 mb-2 p-2 bg-white rounded flex-wrap">
<select
value={dev.dayOffset || 0}
onChange={e => onDeviationChange(index, 'dayOffset', parseInt(e.target.value, 10))}
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>
))}
</select>
<TimeInput
value={dev.time}
onChange={newTime => onDeviationChange(index, 'time', newTime)}
/>
<div className="w-32">
<NumericInput
value={dev.dose}
onChange={newDose => onDeviationChange(index, 'dose', newDose)}
increment={doseIncrement}
min={0}
unit="mg"
/>
</div>
<button
onClick={() => onRemoveDeviation(index)}
className="text-red-500 hover:text-red-700 font-bold text-lg"
>
&times;
</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.">
<input
type="checkbox"
id={`add_dose_${index}`}
checked={dev.isAdditional}
onChange={e => onDeviationChange(index, 'isAdditional', e.target.checked)}
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
</label>
</div>
</div>
))}
<button
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
</button>
</div>
);
};
export default DeviationList;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import TimeInput from './TimeInput.js';
import NumericInput from './NumericInput.js';
const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses }) => {
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>
{doses.map((dose, index) => (
<div key={index} className="flex items-center space-x-3 mb-3">
<TimeInput
value={dose.time}
onChange={newTime => onUpdateDoses(doses.map((d, i) => i === index ? {...d, time: newTime} : d))}
/>
<div className="w-40">
<NumericInput
value={dose.dose}
onChange={newDose => onUpdateDoses(doses.map((d, i) => i === index ? {...d, dose: newDose} : d))}
increment={doseIncrement}
min={0}
unit="mg"
/>
</div>
<span className="text-gray-600 text-sm flex-1">{dose.label}</span>
</div>
))}
</div>
);
};
export default DoseSchedule;

View File

@@ -0,0 +1,55 @@
import React from 'react';
const NumericInput = ({ value, onChange, increment, min = -Infinity, max = Infinity, placeholder, unit }) => {
const updateValue = (direction) => {
const numIncrement = parseFloat(increment) || 1;
let numValue = parseFloat(value) || 0;
numValue += direction * numIncrement;
numValue = Math.max(min, numValue);
numValue = Math.min(max, numValue);
const finalValue = String(Math.round(numValue * 100) / 100);
onChange(finalValue);
};
const handleKeyDown = (e) => {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault();
updateValue(e.key === 'ArrowUp' ? 1 : -1);
}
};
const handleChange = (e) => {
const val = e.target.value;
if (val === '' || /^-?\d*\.?\d*$/.test(val)) {
onChange(val);
}
};
return (
<div className="flex items-center w-full">
<button
onClick={() => updateValue(-1)}
className="px-2 py-1 border rounded-l-md bg-gray-100 hover:bg-gray-200 text-lg font-bold"
>
-
</button>
<input
type="text"
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className="p-2 border-t border-b w-full text-sm text-center"
/>
<button
onClick={() => updateValue(1)}
className="px-2 py-1 border rounded-r-md bg-gray-100 hover:bg-gray-200 text-lg font-bold"
>
+
</button>
{unit && <span className="ml-2 text-gray-500 text-sm whitespace-nowrap">{unit}</span>}
</div>
);
};
export default NumericInput;

153
src/components/Settings.js Normal file
View File

@@ -0,0 +1,153 @@
import React from 'react';
import NumericInput from './NumericInput.js';
const Settings = ({
pkParams,
therapeuticRange,
uiSettings,
onUpdatePkParams,
onUpdateTherapeuticRange,
onUpdateUiSetting,
onReset
}) => {
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>
<div className="space-y-4 text-sm">
<div className="flex items-center">
<input
type="checkbox"
id="showDayTimeXAxis"
checked={showDayTimeXAxis}
onChange={e => onUpdateUiSetting('showDayTimeXAxis', e.target.checked)}
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
</label>
</div>
<label className="block font-medium text-gray-600 pt-2">Simulationsdauer</label>
<div className="w-40">
<NumericInput
value={simulationDays}
onChange={val => onUpdateUiSetting('simulationDays', val)}
increment={'1'}
min={2}
max={7}
unit="Tage"
/>
</div>
<label className="block font-medium text-gray-600">Angezeigte Tage</label>
<div className="w-40">
<NumericInput
value={displayedDays}
onChange={val => onUpdateUiSetting('displayedDays', val)}
increment={'1'}
min={1}
max={parseInt(simulationDays, 10) || 1}
unit="Tage"
/>
</div>
<label className="block font-medium text-gray-600 pt-2">Y-Achsen-Bereich</label>
<div className="flex items-center space-x-2 mt-1">
<div className="w-32">
<NumericInput
value={yAxisMin}
onChange={val => onUpdateUiSetting('yAxisMin', val)}
increment={'5'}
min={0}
placeholder="Auto"
unit="ng/ml"
/>
</div>
<span className="text-gray-500">-</span>
<div className="w-32">
<NumericInput
value={yAxisMax}
onChange={val => onUpdateUiSetting('yAxisMax', val)}
increment={'5'}
min={0}
placeholder="Auto"
unit="ng/ml"
/>
</div>
</div>
<label className="block font-medium text-gray-600">Therapeutischer Bereich</label>
<div className="flex items-center space-x-2 mt-1">
<div className="w-32">
<NumericInput
value={therapeuticRange.min}
onChange={val => onUpdateTherapeuticRange('min', val)}
increment={'0.5'}
min={0}
placeholder="Min"
unit="ng/ml"
/>
</div>
<span className="text-gray-500">-</span>
<div className="w-32">
<NumericInput
value={therapeuticRange.max}
onChange={val => onUpdateTherapeuticRange('max', val)}
increment={'0.5'}
min={0}
placeholder="Max"
unit="ng/ml"
/>
</div>
</div>
<h3 className="text-lg font-semibold mt-4 pt-4 border-t">d-Amphetamin Parameter</h3>
<div className="w-40">
<label className="block font-medium text-gray-600">Halbwertszeit</label>
<NumericInput
value={pkParams.damph.halfLife}
onChange={val => onUpdatePkParams('damph', { halfLife: val })}
increment={'0.5'}
min={0.1}
unit="h"
/>
</div>
<h3 className="text-lg font-semibold mt-4 pt-4 border-t">Lisdexamfetamin Parameter</h3>
<div className="w-40">
<label className="block font-medium text-gray-600">Umwandlungs-Halbwertszeit</label>
<NumericInput
value={pkParams.ldx.halfLife}
onChange={val => onUpdatePkParams('ldx', { halfLife: val })}
increment={'0.1'}
min={0.1}
unit="h"
/>
</div>
<div className="w-40">
<label className="block font-medium text-gray-600">Absorptionsrate</label>
<NumericInput
value={pkParams.ldx.absorptionRate}
onChange={val => onUpdatePkParams('ldx', { absorptionRate: val })}
increment={'0.1'}
min={0.1}
unit="(schneller >)"
/>
</div>
<div className="pt-4">
<button
onClick={onReset}
className="w-full bg-red-600 text-white py-2 rounded-md hover:bg-red-700 text-sm"
>
Alle Einstellungen zurücksetzen
</button>
</div>
</div>
</div>
);
};
export default Settings;

View File

@@ -0,0 +1,181 @@
import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer } from 'recharts';
const SimulationChart = ({
idealProfile,
deviatedProfile,
correctedProfile,
chartView,
showDayTimeXAxis,
therapeuticRange,
simulationDays,
displayedDays,
yAxisMin,
yAxisMax
}) => {
const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
const chartTicks = Array.from({length: Math.floor(totalHours / 6) + 1}, (_, i) => i * 6);
const chartWidthPercentage = Math.max(100, (totalHours / ( (parseInt(displayedDays, 10) || 2) * 24)) * 100);
const chartDomain = React.useMemo(() => {
const numMin = parseFloat(yAxisMin);
const numMax = parseFloat(yAxisMax);
const domainMin = !isNaN(numMin) ? numMin : 'auto';
const domainMax = !isNaN(numMax) ? numMax : 'auto';
return [domainMin, domainMax];
}, [yAxisMin, yAxisMax]);
return (
<div className="flex-grow w-full overflow-x-auto">
<div style={{ width: `${chartWidthPercentage}%`, height: '100%', minWidth: '100%' }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart margin={{ top: 20, right: 20, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="timeHours"
type="number"
domain={[0, totalHours]}
ticks={chartTicks}
tickFormatter={(h) => `${h}h`}
xAxisId="continuous"
/>
{showDayTimeXAxis && (
<XAxis
dataKey="timeHours"
type="number"
domain={[0, totalHours]}
ticks={chartTicks}
tickFormatter={(h) => `${h % 24}h`}
xAxisId="daytime"
orientation="top"
/>
)}
<YAxis
label={{ value: 'Konzentration (ng/ml)', 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`}
/>
<Legend verticalAlign="top" height={36} />
{(chartView === 'damph' || chartView === 'both') && (
<ReferenceLine
y={parseFloat(therapeuticRange.min) || 0}
label={{ value: 'Min', position: 'insideTopLeft' }}
stroke="green"
strokeDasharray="3 3"
xAxisId="continuous"
/>
)}
{(chartView === 'damph' || chartView === 'both') && (
<ReferenceLine
y={parseFloat(therapeuticRange.max) || 0}
label={{ value: 'Max', position: 'insideTopLeft' }}
stroke="red"
strokeDasharray="3 3"
xAxisId="continuous"
/>
)}
{[...Array(parseInt(simulationDays, 10) || 0).keys()].map(day => (
day > 0 && (
<ReferenceLine
key={day}
x={day * 24}
stroke="#999"
strokeDasharray="5 5"
xAxisId="continuous"
/>
)
))}
{(chartView === 'damph' || chartView === 'both') && (
<Line
type="monotone"
data={idealProfile}
dataKey="damph"
name="d-Amphetamin (Ideal)"
stroke="#3b82f6"
strokeWidth={2.5}
dot={false}
xAxisId="continuous"
/>
)}
{(chartView === 'ldx' || chartView === 'both') && (
<Line
type="monotone"
data={idealProfile}
dataKey="ldx"
name="Lisdexamfetamin (Ideal)"
stroke="#8b5cf6"
strokeWidth={2}
dot={false}
strokeDasharray="3 3"
xAxisId="continuous"
/>
)}
{deviatedProfile && (chartView === 'damph' || chartView === 'both') && (
<Line
type="monotone"
data={deviatedProfile}
dataKey="damph"
name="d-Amphetamin (Abweichung)"
stroke="#f59e0b"
strokeWidth={2}
strokeDasharray="5 5"
dot={false}
xAxisId="continuous"
/>
)}
{deviatedProfile && (chartView === 'ldx' || chartView === 'both') && (
<Line
type="monotone"
data={deviatedProfile}
dataKey="ldx"
name="Lisdexamfetamin (Abweichung)"
stroke="#f97316"
strokeWidth={1.5}
strokeDasharray="5 5"
dot={false}
xAxisId="continuous"
/>
)}
{correctedProfile && (chartView === 'damph' || chartView === 'both') && (
<Line
type="monotone"
data={correctedProfile}
dataKey="damph"
name="d-Amphetamin (Korrektur)"
stroke="#10b981"
strokeWidth={2.5}
strokeDasharray="3 7"
dot={false}
xAxisId="continuous"
/>
)}
{correctedProfile && (chartView === 'ldx' || chartView === 'both') && (
<Line
type="monotone"
data={correctedProfile}
dataKey="ldx"
name="Lisdexamfetamin (Korrektur)"
stroke="#059669"
strokeWidth={2}
strokeDasharray="3 7"
dot={false}
xAxisId="continuous"
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
};
export default SimulationChart;

View File

@@ -0,0 +1,28 @@
import React from 'react';
const SuggestionPanel = ({ suggestion, onApplySuggestion }) => {
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>
{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>.
</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
</button>
</>
) : (
<p className="text-sm text-sky-800">{suggestion.text}</p>
)}
</div>
);
};
export default SuggestionPanel;

109
src/components/TimeInput.js Normal file
View File

@@ -0,0 +1,109 @@
import React from 'react';
const TimeInput = ({ value, onChange }) => {
const [displayValue, setDisplayValue] = React.useState(value);
const [isPickerOpen, setIsPickerOpen] = React.useState(false);
const [pickerHours, pickerMinutes] = (value || "00:00").split(':').map(Number);
React.useEffect(() => {
setDisplayValue(value);
}, [value]);
const handleBlur = (e) => {
let input = e.target.value.replace(/[^0-9]/g, '');
let hours = '00', minutes = '00';
if (input.length <= 2) {
hours = input.padStart(2, '0');
}
else if (input.length === 3) {
hours = input.substring(0, 1).padStart(2, '0');
minutes = input.substring(1, 3);
}
else {
hours = input.substring(0, 2);
minutes = input.substring(2, 4);
}
hours = Math.min(23, parseInt(hours, 10) || 0).toString().padStart(2, '0');
minutes = Math.min(59, parseInt(minutes, 10) || 0).toString().padStart(2, '0');
const formattedTime = `${hours}:${minutes}`;
setDisplayValue(formattedTime);
onChange(formattedTime);
};
const handleChange = (e) => {
setDisplayValue(e.target.value);
};
const handlePickerChange = (part, val) => {
let newHours = pickerHours, newMinutes = pickerMinutes;
if (part === 'h') {
newHours = val;
} else {
newMinutes = val;
}
const formattedTime = `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}`;
onChange(formattedTime);
};
return (
<div className="relative flex items-center">
<input
type="text"
value={displayValue}
onChange={handleChange}
onBlur={handleBlur}
placeholder="HH:MM"
className="p-2 border rounded-md w-24 text-sm text-center"
/>
<button
onClick={() => setIsPickerOpen(!isPickerOpen)}
className="ml-2 p-2 text-gray-500 hover:text-gray-700"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.414-1.415L11 9.586V6z" clipRule="evenodd" />
</svg>
</button>
{isPickerOpen && (
<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="grid grid-cols-6 gap-1">
{[...Array(24).keys()].map(h => (
<button
key={h}
onClick={() => handlePickerChange('h', h)}
className={`p-1 rounded text-xs ${h === pickerHours ? 'bg-sky-500 text-white' : 'bg-gray-200 hover:bg-sky-200'}`}
>
{String(h).padStart(2,'0')}
</button>
))}
</div>
</div>
<div className="mt-4">
<div className="mb-2"><span className="font-semibold">Minute:</span></div>
<div className="grid grid-cols-4 gap-1">
{[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55].map(m => (
<button
key={m}
onClick={() => handlePickerChange('m', m)}
className={`p-1 rounded text-xs ${m === pickerMinutes ? 'bg-sky-500 text-white' : 'bg-gray-200 hover:bg-sky-200'}`}
>
{String(m).padStart(2,'0')}
</button>
))}
</div>
</div>
<button
onClick={() => setIsPickerOpen(false)}
className="mt-4 w-full bg-gray-600 text-white py-1 rounded-md text-sm"
>
Schließen
</button>
</div>
)}
</div>
);
};
export default TimeInput;