diff --git a/package.json b/package.json
index 3fd63dc..40cbbb7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "med-plan-assistant",
- "version": "0.1.0",
+ "version": "0.1.1",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.1",
diff --git a/src/App.js b/src/App.js
index 91ea3ff..f0b03ae 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,352 +1,54 @@
import React from 'react';
-import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer } from 'recharts';
-// --- Constants ---
-const LOCAL_STORAGE_KEY = 'medPlanAssistantState_v5';
-const LDX_TO_DAMPH_CONVERSION_FACTOR = 0.2948;
-
-
-// --- Helper Functions ---
-const timeToMinutes = (timeStr) => {
- if (!timeStr || !timeStr.includes(':')) return 0;
- const [hours, minutes] = timeStr.split(':').map(Number);
- return hours * 60 + minutes;
-};
-
-// --- Default State ---
-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' },
- ],
- steadyStateConfig: { daysOnMedication: '7' },
- therapeuticRange: { min: '11.5', max: '14' },
- doseIncrement: '2.5',
- uiSettings: {
- showDayTimeXAxis: true,
- chartView: 'damph',
- yAxisMin: '',
- yAxisMax: '',
- simulationDays: '3',
- displayedDays: '2',
- }
-});
-
-// --- Custom Components ---
-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 (
-
-
-
- {isPickerOpen && (
-
-
{value}
-
-
Stunde:
-
{[...Array(24).keys()].map(h => ())}
-
-
-
Minute:
-
{[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55].map(m => ())}
-
-
-
- )}
-
- );
-};
-
-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 (
-
-
-
-
- {unit && {unit}}
-
- );
-};
+// Components
+import DoseSchedule from './components/DoseSchedule.js';
+import DeviationList from './components/DeviationList.js';
+import SuggestionPanel from './components/SuggestionPanel.js';
+import SimulationChart from './components/SimulationChart.js';
+import Settings from './components/Settings.js';
+// Custom Hooks
+import { useAppState } from './hooks/useAppState.js';
+import { useSimulation } from './hooks/useSimulation.js';
// --- Main Component ---
const MedPlanAssistant = () => {
- const [appState, setAppState] = React.useState(getDefaultState);
- const [isLoaded, setIsLoaded] = React.useState(false);
+ const {
+ appState,
+ updateState,
+ updateNestedState,
+ updateUiSetting,
+ handleReset
+ } = useAppState();
- React.useEffect(() => {
- try {
- const savedState = window.localStorage.getItem(LOCAL_STORAGE_KEY);
- if (savedState) {
- const parsedState = JSON.parse(savedState);
- const defaults = getDefaultState();
- setAppState({
- ...defaults, ...parsedState,
- pkParams: {...defaults.pkParams, ...parsedState.pkParams},
- uiSettings: {...defaults.uiSettings, ...parsedState.uiSettings},
- });
- }
- } catch (error) { console.error("Failed to load state", error); }
- setIsLoaded(true);
- }, []);
+ const {
+ pkParams,
+ doses,
+ therapeuticRange,
+ doseIncrement,
+ uiSettings
+ } = appState;
+
+ const {
+ showDayTimeXAxis,
+ chartView,
+ yAxisMin,
+ yAxisMax,
+ simulationDays,
+ displayedDays
+ } = uiSettings;
- React.useEffect(() => {
- if (isLoaded) {
- try {
- const stateToSave = {
- pkParams: appState.pkParams,
- doses: appState.doses,
- steadyStateConfig: appState.steadyStateConfig,
- therapeuticRange: appState.therapeuticRange,
- doseIncrement: appState.doseIncrement,
- uiSettings: appState.uiSettings,
- };
- window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(stateToSave));
- } catch (error) { console.error("Failed to save state", error); }
- }
- }, [appState, isLoaded]);
-
- const { pkParams, doses, steadyStateConfig, therapeuticRange, doseIncrement, uiSettings } = appState;
- const { showDayTimeXAxis, chartView, yAxisMin, yAxisMax, simulationDays, displayedDays } = uiSettings;
-
- const [deviations, setDeviations] = React.useState([]);
- const [suggestion, setSuggestion] = React.useState(null);
-
- const updateState = (key, value) => { setAppState(prev => ({ ...prev, [key]: value })); };
- const updateNestedState = (parentKey, childKey, value) => { setAppState(prev => ({ ...prev, [parentKey]: { ...prev[parentKey], ...value } })); };
- const updateUiSetting = (key, value) => {
- const newUiSettings = { ...appState.uiSettings, [key]: value };
- if (key === 'simulationDays') {
- const simDaysNum = parseInt(value, 10) || 1;
- const dispDaysNum = parseInt(newUiSettings.displayedDays, 10) || 1;
- if (dispDaysNum > simDaysNum) {
- newUiSettings.displayedDays = String(simDaysNum);
- }
- }
- setAppState(prev => ({ ...prev, uiSettings: newUiSettings }));
- };
-
- const calculateSingleDoseConcentration = React.useCallback((dose, timeSinceDoseHours) => {
- const numDose = parseFloat(dose) || 0;
- if (timeSinceDoseHours < 0 || numDose <= 0) return { ldx: 0, damph: 0 };
- const ka_ldx = Math.log(2) / (parseFloat(pkParams.ldx.absorptionRate) || 1);
- const k_conv = Math.log(2) / (parseFloat(pkParams.ldx.halfLife) || 1);
- const ke_damph = Math.log(2) / (parseFloat(pkParams.damph.halfLife) || 1);
- let ldxConcentration = 0;
- if (Math.abs(ka_ldx - k_conv) > 0.0001) {
- ldxConcentration = (numDose * ka_ldx / (ka_ldx - k_conv)) * (Math.exp(-k_conv * timeSinceDoseHours) - Math.exp(-ka_ldx * timeSinceDoseHours));
- }
- let damphConcentration = 0;
- if (Math.abs(ka_ldx - ke_damph) > 0.0001 && Math.abs(k_conv - ke_damph) > 0.0001 && Math.abs(ka_ldx - k_conv) > 0.0001) {
- const term1 = Math.exp(-ke_damph * timeSinceDoseHours) / ((ka_ldx - ke_damph) * (k_conv - ke_damph));
- const term2 = Math.exp(-k_conv * timeSinceDoseHours) / ((ka_ldx - k_conv) * (ke_damph - k_conv));
- const term3 = Math.exp(-ka_ldx * timeSinceDoseHours) / ((k_conv - ka_ldx) * (ke_damph - ka_ldx));
- damphConcentration = LDX_TO_DAMPH_CONVERSION_FACTOR * numDose * ka_ldx * k_conv * (term1 + term2 + term3);
- }
- return { ldx: Math.max(0, ldxConcentration), damph: Math.max(0, damphConcentration) };
- }, [pkParams]);
-
- const calculateCombinedProfile = React.useCallback((doseSchedule, deviationList = [], correction = null) => {
- const dataPoints = [];
- const timeStepHours = 0.25;
- const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
- const daysToSimulate = Math.min(parseInt(steadyStateConfig.daysOnMedication, 10) || 0, 5);
- for (let t = 0; t <= totalHours; t += timeStepHours) {
- let totalLdx = 0;
- let totalDamph = 0;
- let allDoses = [];
- const maxDayOffset = (parseInt(simulationDays, 10) || 3) -1;
- for (let day = -daysToSimulate; day <= maxDayOffset; day++) {
- const dayOffset = day * 24 * 60;
- doseSchedule.forEach(d => { allDoses.push({ ...d, time: timeToMinutes(d.time) + dayOffset, isPlan: true }); });
- }
-
- const currentDeviations = [...deviationList];
- if (correction) {
- currentDeviations.push({ ...correction, isAdditional: true });
- }
-
- currentDeviations.forEach(dev => {
- const devTime = timeToMinutes(dev.time) + (dev.dayOffset || 0) * 24 * 60;
- if (!dev.isAdditional) {
- const closestDoseIndex = allDoses.reduce((closest, dose, index) => {
- if (!dose.isPlan) return closest;
- const diff = Math.abs(dose.time - devTime);
- if (diff <= 60 && diff < closest.minDiff) { return { index, minDiff: diff }; }
- return closest;
- }, { index: -1, minDiff: 61 }).index;
- if (closestDoseIndex !== -1) { allDoses.splice(closestDoseIndex, 1); }
- }
- allDoses.push({ ...dev, time: devTime });
- });
-
- allDoses.forEach(doseInfo => {
- const timeSinceDoseHours = t - doseInfo.time / 60;
- const concentrations = calculateSingleDoseConcentration(doseInfo.dose, timeSinceDoseHours);
- totalLdx += concentrations.ldx;
- totalDamph += concentrations.damph;
- });
- dataPoints.push({ timeHours: t, ldx: totalLdx, damph: totalDamph });
- }
- return dataPoints;
- }, [steadyStateConfig, calculateSingleDoseConcentration, simulationDays]);
-
- const generateSuggestion = React.useCallback(() => {
- if (deviations.length === 0) {
- setSuggestion(null);
- return;
- }
- const lastDeviation = [...deviations].sort((a, b) => timeToMinutes(a.time) + (a.dayOffset || 0) * 1440 - (timeToMinutes(b.time) + (b.dayOffset || 0) * 1440)).pop();
- const deviationTimeTotalMinutes = timeToMinutes(lastDeviation.time) + (lastDeviation.dayOffset || 0) * 1440;
-
- let nextDose = null;
- let minDiff = Infinity;
-
- doses.forEach(d => {
- const doseTimeInMinutes = timeToMinutes(d.time);
- for (let i=0; i < (parseInt(simulationDays, 10) || 1); i++) {
- const absoluteTime = doseTimeInMinutes + i * 1440;
- const diff = absoluteTime - deviationTimeTotalMinutes;
- if (diff > 0 && diff < minDiff) {
- minDiff = diff;
- nextDose = {...d, dayOffset: i};
- }
- }
- });
-
- if (!nextDose) {
- setSuggestion({ text: "Keine passende nächste Dosis für Korrektur gefunden." });
- return;
- }
-
- const numDoseIncrement = parseFloat(doseIncrement) || 1;
- const idealProfile = calculateCombinedProfile(doses);
- const deviatedProfile = calculateCombinedProfile(doses, deviations);
-
- const nextDoseTimeHours = (timeToMinutes(nextDose.time) + (nextDose.dayOffset || 0) * 1440) / 60;
-
- const idealConcentration = idealProfile.find(p => Math.abs(p.timeHours - nextDoseTimeHours) < 0.1)?.damph || 0;
- const deviatedConcentration = deviatedProfile.find(p => Math.abs(p.timeHours - nextDoseTimeHours) < 0.1)?.damph || 0;
- const concentrationDifference = idealConcentration - deviatedConcentration;
-
- if (Math.abs(concentrationDifference) < 0.5) {
- setSuggestion({ text: "Keine signifikante Korrektur notwendig." });
- return;
- }
-
- const doseAdjustmentFactor = 0.5;
- let doseChange = concentrationDifference / doseAdjustmentFactor;
- doseChange = Math.round(doseChange / numDoseIncrement) * numDoseIncrement;
- let suggestedDoseValue = (parseFloat(nextDose.dose) || 0) + doseChange;
- suggestedDoseValue = Math.max(0, Math.min(70, suggestedDoseValue));
-
- setSuggestion({
- time: nextDose.time,
- dose: String(suggestedDoseValue),
- isAdditional: false,
- originalDose: nextDose.dose,
- dayOffset: nextDose.dayOffset
- });
- }, [doses, deviations, calculateCombinedProfile, doseIncrement, simulationDays]);
-
- React.useEffect(() => {
- generateSuggestion();
- }, [deviations, doses, pkParams, doseIncrement, generateSuggestion]);
-
-
- const idealProfile = React.useMemo(() => calculateCombinedProfile(doses), [doses, calculateCombinedProfile]);
- const deviatedProfile = React.useMemo(() => deviations.length > 0 ? calculateCombinedProfile(doses, deviations) : null, [doses, deviations, calculateCombinedProfile]);
- const correctedProfile = React.useMemo(() => suggestion && suggestion.dose ? calculateCombinedProfile(doses, deviations, suggestion) : null, [doses, deviations, suggestion, calculateCombinedProfile]);
-
- const handleReset = () => {
- if (window.confirm("Bist du sicher, dass du alle Einstellungen auf die Standardwerte zurücksetzen möchtest? Dies kann nicht rückgängig gemacht werden.")) {
- window.localStorage.removeItem(LOCAL_STORAGE_KEY);
- window.location.reload();
- }
- };
-
- const addDeviation = () => {
- const sortedDoses = [...doses].sort((a,b) => timeToMinutes(a.time) - timeToMinutes(b.time));
- let nextDose = sortedDoses[0] || { time: '08:00', dose: '25' };
- if (deviations.length > 0) {
- const lastDev = deviations[deviations.length - 1];
- const lastDevTime = timeToMinutes(lastDev.time) + (lastDev.dayOffset || 0) * 24 * 60;
- const nextPlanned = sortedDoses.find(d => timeToMinutes(d.time) > (lastDevTime % (24*60)));
- if (nextPlanned) {
- nextDose = { ...nextPlanned, dayOffset: lastDev.dayOffset };
- } else {
- nextDose = { ...sortedDoses[0], dayOffset: (lastDev.dayOffset || 0) + 1 };
- }
- }
- setDeviations([...deviations, { ...nextDose, isAdditional: false, dayOffset: nextDose.dayOffset || 0 }]);
- };
-
- const removeDeviation = (index) => { setDeviations(deviations.filter((_, i) => i !== index)); };
- const handleDeviationChange = (index, field, value) => {
- const newDeviations = [...deviations];
- newDeviations[index][field] = value;
- setDeviations(newDeviations);
- };
-
- const applySuggestion = () => {
- if (!suggestion || !suggestion.dose) return;
- setDeviations([...deviations, suggestion]);
- setSuggestion(null);
- }
-
- 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]);
-
- 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 {
+ deviations,
+ suggestion,
+ idealProfile,
+ deviatedProfile,
+ correctedProfile,
+ addDeviation,
+ removeDeviation,
+ handleDeviationChange,
+ applySuggestion
+ } = useSimulation(appState);
return (
@@ -355,134 +57,89 @@ const MedPlanAssistant = () => {
Medikationsplan-Assistent
Simulation für Lisdexamfetamin (LDX) und d-Amphetamin (d-amph)
+
+ {/* Left Column - Controls */}
-
-
Mein Plan
- {doses.map((dose, index) => (
-
-
updateState('doses', doses.map((d, i) => i === index ? {...d, time: newTime} : d))} />
-
- updateState('doses', doses.map((d, i) => i === index ? {...d, dose: newDose} : d))} increment={doseIncrement} min={0} unit="mg" />
-
- {dose.label}
-
- ))}
-
+
updateState('doses', newDoses)}
+ />
-
-
Abweichungen vom Plan
- {deviations.map((dev, index) => (
-
-
-
handleDeviationChange(index, 'time', newTime)} />
-
- handleDeviationChange(index, 'dose', newDose)} increment={doseIncrement} min={0} unit="mg"/>
-
-
-
- handleDeviationChange(index, 'isAdditional', e.target.checked)} className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500" />
-
-
-
- ))}
-
-
-
- {suggestion && (
-
-
Was wäre wenn?
- {suggestion.dose ? (
- <>
-
Vorschlag: {suggestion.dose}mg (statt {suggestion.originalDose}mg) um {suggestion.time}.
-
- >
- ) : (
-
{suggestion.text}
- )}
-
- )}
+
+
+
+ {/* Center Column - Chart */}
-
-
-
-
-
-
-
-
-
- `${h}h`} xAxisId="continuous" />
- {showDayTimeXAxis && `${h % 24}h`} xAxisId="daytime" orientation="top" />}
-
- [`${value.toFixed(1)} ng/ml`, name]} labelFormatter={(label) => `Stunde: ${label}h`}/>
-
- {(chartView === 'damph' || chartView === 'both') && }
- {(chartView === 'damph' || chartView === 'both') && }
-
- {[...Array(parseInt(simulationDays, 10) || 0).keys()].map(day => (
- day > 0 &&
- ))}
-
- {(chartView === 'damph' || chartView === 'both') && }
- {(chartView === 'ldx' || chartView === 'both') && }
-
- {deviatedProfile && (chartView === 'damph' || chartView === 'both') && }
- {deviatedProfile && (chartView === 'ldx' || chartView === 'both') && }
-
- {correctedProfile && (chartView === 'damph' || chartView === 'both') && }
- {correctedProfile && (chartView === 'ldx' || chartView === 'both') && }
-
-
-
-
+
+
+
+
+
+
+ {/* Right Column - Settings */}
-
-
Erweiterte Einstellungen
-
-
updateUiSetting('showDayTimeXAxis', e.target.checked)} className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500" />
-
updateUiSetting('simulationDays', val)} increment={'1'} min={2} max={7} unit="Tage"/>
-
updateUiSetting('displayedDays', val)} increment={'1'} min={1} max={parseInt(simulationDays, 10) || 1} unit="Tage"/>
-
-
-
updateUiSetting('yAxisMin', val)} increment={'5'} min={0} placeholder="Auto" unit="ng/ml"/>
-
-
-
updateUiSetting('yAxisMax', val)} increment={'5'} min={0} placeholder="Auto" unit="ng/ml"/>
-
-
-
-
updateNestedState('therapeuticRange', 'min', val)} increment={'0.5'} min={0} placeholder="Min" unit="ng/ml"/>
-
-
-
updateNestedState('therapeuticRange', 'max', val)} increment={'0.5'} min={0} placeholder="Max" unit="ng/ml"/>
-
-
d-Amphetamin Parameter
-
updateNestedState('pkParams', 'damph', { halfLife: val })} increment={'0.5'} min={0.1} unit="h"/>
-
Lisdexamfetamin Parameter
-
updateNestedState('pkParams', 'ldx', { halfLife: val })} increment={'0.1'} min={0.1} unit="h"/>
-
updateNestedState('pkParams', 'ldx', { absorptionRate: val })} increment={'0.1'} min={0.1} unit="(schneller >)"/>
-
-
-
-
-
+
updateNestedState('pkParams', key, value)}
+ onUpdateTherapeuticRange={(key, value) => updateNestedState('therapeuticRange', key, { [key]: value })}
+ onUpdateUiSetting={updateUiSetting}
+ onReset={handleReset}
+ />
+
- )
+ );
};
-export default MedPlanAssistant;
+export default MedPlanAssistant;
\ No newline at end of file
diff --git a/src/components/DeviationList.js b/src/components/DeviationList.js
new file mode 100644
index 0000000..fd60637
--- /dev/null
+++ b/src/components/DeviationList.js
@@ -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 (
+
+
Abweichungen vom Plan
+ {deviations.map((dev, index) => (
+
+
+
onDeviationChange(index, 'time', newTime)}
+ />
+
+ onDeviationChange(index, 'dose', newDose)}
+ increment={doseIncrement}
+ min={0}
+ unit="mg"
+ />
+
+
+
+ onDeviationChange(index, 'isAdditional', e.target.checked)}
+ className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
+ />
+
+
+
+ ))}
+
+
+ );
+};
+
+export default DeviationList;
diff --git a/src/components/DoseSchedule.js b/src/components/DoseSchedule.js
new file mode 100644
index 0000000..b78d996
--- /dev/null
+++ b/src/components/DoseSchedule.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import TimeInput from './TimeInput.js';
+import NumericInput from './NumericInput.js';
+
+const DoseSchedule = ({ doses, doseIncrement, onUpdateDoses }) => {
+ return (
+
+
Mein Plan
+ {doses.map((dose, index) => (
+
+
onUpdateDoses(doses.map((d, i) => i === index ? {...d, time: newTime} : d))}
+ />
+
+ onUpdateDoses(doses.map((d, i) => i === index ? {...d, dose: newDose} : d))}
+ increment={doseIncrement}
+ min={0}
+ unit="mg"
+ />
+
+ {dose.label}
+
+ ))}
+
+ );
+};
+
+export default DoseSchedule;
diff --git a/src/components/NumericInput.js b/src/components/NumericInput.js
new file mode 100644
index 0000000..9184f3c
--- /dev/null
+++ b/src/components/NumericInput.js
@@ -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 (
+
+
+
+
+ {unit && {unit}}
+
+ );
+};
+
+export default NumericInput;
diff --git a/src/components/Settings.js b/src/components/Settings.js
new file mode 100644
index 0000000..154830c
--- /dev/null
+++ b/src/components/Settings.js
@@ -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 (
+
+
Erweiterte Einstellungen
+
+
+ onUpdateUiSetting('showDayTimeXAxis', e.target.checked)}
+ className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
+ />
+
+
+
+
+
+ onUpdateUiSetting('simulationDays', val)}
+ increment={'1'}
+ min={2}
+ max={7}
+ unit="Tage"
+ />
+
+
+
+
+ onUpdateUiSetting('displayedDays', val)}
+ increment={'1'}
+ min={1}
+ max={parseInt(simulationDays, 10) || 1}
+ unit="Tage"
+ />
+
+
+
+
+
+ onUpdateUiSetting('yAxisMin', val)}
+ increment={'5'}
+ min={0}
+ placeholder="Auto"
+ unit="ng/ml"
+ />
+
+
-
+
+ onUpdateUiSetting('yAxisMax', val)}
+ increment={'5'}
+ min={0}
+ placeholder="Auto"
+ unit="ng/ml"
+ />
+
+
+
+
+
+
+ onUpdateTherapeuticRange('min', val)}
+ increment={'0.5'}
+ min={0}
+ placeholder="Min"
+ unit="ng/ml"
+ />
+
+
-
+
+ onUpdateTherapeuticRange('max', val)}
+ increment={'0.5'}
+ min={0}
+ placeholder="Max"
+ unit="ng/ml"
+ />
+
+
+
+
d-Amphetamin Parameter
+
+
+ onUpdatePkParams('damph', { halfLife: val })}
+ increment={'0.5'}
+ min={0.1}
+ unit="h"
+ />
+
+
+
Lisdexamfetamin Parameter
+
+
+ onUpdatePkParams('ldx', { halfLife: val })}
+ increment={'0.1'}
+ min={0.1}
+ unit="h"
+ />
+
+
+
+ onUpdatePkParams('ldx', { absorptionRate: val })}
+ increment={'0.1'}
+ min={0.1}
+ unit="(schneller >)"
+ />
+
+
+
+
+
+
+
+ );
+};
+
+export default Settings;
diff --git a/src/components/SimulationChart.js b/src/components/SimulationChart.js
new file mode 100644
index 0000000..33de73c
--- /dev/null
+++ b/src/components/SimulationChart.js
@@ -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 (
+
+
+
+
+
+ `${h}h`}
+ xAxisId="continuous"
+ />
+ {showDayTimeXAxis && (
+ `${h % 24}h`}
+ xAxisId="daytime"
+ orientation="top"
+ />
+ )}
+
+ [`${value.toFixed(1)} ng/ml`, name]}
+ labelFormatter={(label) => `Stunde: ${label}h`}
+ />
+
+
+ {(chartView === 'damph' || chartView === 'both') && (
+
+ )}
+ {(chartView === 'damph' || chartView === 'both') && (
+
+ )}
+
+ {[...Array(parseInt(simulationDays, 10) || 0).keys()].map(day => (
+ day > 0 && (
+
+ )
+ ))}
+
+ {(chartView === 'damph' || chartView === 'both') && (
+
+ )}
+ {(chartView === 'ldx' || chartView === 'both') && (
+
+ )}
+
+ {deviatedProfile && (chartView === 'damph' || chartView === 'both') && (
+
+ )}
+ {deviatedProfile && (chartView === 'ldx' || chartView === 'both') && (
+
+ )}
+
+ {correctedProfile && (chartView === 'damph' || chartView === 'both') && (
+
+ )}
+ {correctedProfile && (chartView === 'ldx' || chartView === 'both') && (
+
+ )}
+
+
+
+
+ );
+};
+
+export default SimulationChart;
diff --git a/src/components/SuggestionPanel.js b/src/components/SuggestionPanel.js
new file mode 100644
index 0000000..38b7e39
--- /dev/null
+++ b/src/components/SuggestionPanel.js
@@ -0,0 +1,28 @@
+import React from 'react';
+
+const SuggestionPanel = ({ suggestion, onApplySuggestion }) => {
+ if (!suggestion) return null;
+
+ return (
+
+
Was wäre wenn?
+ {suggestion.dose ? (
+ <>
+
+ Vorschlag: {suggestion.dose}mg (statt {suggestion.originalDose}mg) um {suggestion.time}.
+
+
+ >
+ ) : (
+
{suggestion.text}
+ )}
+
+ );
+};
+
+export default SuggestionPanel;
diff --git a/src/components/TimeInput.js b/src/components/TimeInput.js
new file mode 100644
index 0000000..9e34d03
--- /dev/null
+++ b/src/components/TimeInput.js
@@ -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 (
+
+
+
+ {isPickerOpen && (
+
+
{value}
+
+
Stunde:
+
+ {[...Array(24).keys()].map(h => (
+
+ ))}
+
+
+
+
Minute:
+
+ {[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55].map(m => (
+
+ ))}
+
+
+
+
+ )}
+
+ );
+};
+
+export default TimeInput;
diff --git a/src/constants/defaults.js b/src/constants/defaults.js
new file mode 100644
index 0000000..09cc0e8
--- /dev/null
+++ b/src/constants/defaults.js
@@ -0,0 +1,29 @@
+// --- Constants ---
+export const LOCAL_STORAGE_KEY = 'medPlanAssistantState_v5';
+export const LDX_TO_DAMPH_CONVERSION_FACTOR = 0.2948;
+
+// --- Default 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' },
+ ],
+ steadyStateConfig: { daysOnMedication: '7' },
+ therapeuticRange: { min: '11.5', max: '14' },
+ doseIncrement: '2.5',
+ uiSettings: {
+ showDayTimeXAxis: true,
+ chartView: 'damph',
+ yAxisMin: '',
+ yAxisMax: '',
+ simulationDays: '3',
+ displayedDays: '2',
+ }
+});
diff --git a/src/hooks/useAppState.js b/src/hooks/useAppState.js
new file mode 100644
index 0000000..27a0feb
--- /dev/null
+++ b/src/hooks/useAppState.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import { LOCAL_STORAGE_KEY, getDefaultState } from '../constants/defaults.js';
+
+export const useAppState = () => {
+ const [appState, setAppState] = React.useState(getDefaultState);
+ const [isLoaded, setIsLoaded] = React.useState(false);
+
+ React.useEffect(() => {
+ try {
+ const savedState = window.localStorage.getItem(LOCAL_STORAGE_KEY);
+ if (savedState) {
+ const parsedState = JSON.parse(savedState);
+ const defaults = getDefaultState();
+ setAppState({
+ ...defaults,
+ ...parsedState,
+ pkParams: {...defaults.pkParams, ...parsedState.pkParams},
+ uiSettings: {...defaults.uiSettings, ...parsedState.uiSettings},
+ });
+ }
+ } catch (error) {
+ console.error("Failed to load state", error);
+ }
+ setIsLoaded(true);
+ }, []);
+
+ React.useEffect(() => {
+ if (isLoaded) {
+ try {
+ const stateToSave = {
+ pkParams: appState.pkParams,
+ doses: appState.doses,
+ steadyStateConfig: appState.steadyStateConfig,
+ therapeuticRange: appState.therapeuticRange,
+ doseIncrement: appState.doseIncrement,
+ uiSettings: appState.uiSettings,
+ };
+ window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(stateToSave));
+ } catch (error) {
+ console.error("Failed to save state", error);
+ }
+ }
+ }, [appState, isLoaded]);
+
+ const updateState = (key, value) => {
+ setAppState(prev => ({ ...prev, [key]: value }));
+ };
+
+ const updateNestedState = (parentKey, childKey, value) => {
+ setAppState(prev => ({
+ ...prev,
+ [parentKey]: { ...prev[parentKey], [childKey]: value }
+ }));
+ };
+
+ const updateUiSetting = (key, value) => {
+ const newUiSettings = { ...appState.uiSettings, [key]: value };
+ if (key === 'simulationDays') {
+ const simDaysNum = parseInt(value, 10) || 1;
+ const dispDaysNum = parseInt(newUiSettings.displayedDays, 10) || 1;
+ if (dispDaysNum > simDaysNum) {
+ newUiSettings.displayedDays = String(simDaysNum);
+ }
+ }
+ setAppState(prev => ({ ...prev, uiSettings: newUiSettings }));
+ };
+
+ const handleReset = () => {
+ if (window.confirm("Bist du sicher, dass du alle Einstellungen auf die Standardwerte zurücksetzen möchtest? Dies kann nicht rückgängig gemacht werden.")) {
+ window.localStorage.removeItem(LOCAL_STORAGE_KEY);
+ window.location.reload();
+ }
+ };
+
+ return {
+ appState,
+ isLoaded,
+ updateState,
+ updateNestedState,
+ updateUiSetting,
+ handleReset
+ };
+};
diff --git a/src/hooks/useSimulation.js b/src/hooks/useSimulation.js
new file mode 100644
index 0000000..6a9fcf1
--- /dev/null
+++ b/src/hooks/useSimulation.js
@@ -0,0 +1,100 @@
+import React from 'react';
+import { calculateCombinedProfile } from '../utils/calculations.js';
+import { generateSuggestion } from '../utils/suggestions.js';
+import { timeToMinutes } from '../utils/timeUtils.js';
+
+export const useSimulation = (appState) => {
+ const { pkParams, doses, steadyStateConfig, doseIncrement, uiSettings } = appState;
+ const { simulationDays } = uiSettings;
+
+ const [deviations, setDeviations] = React.useState([]);
+ const [suggestion, setSuggestion] = React.useState(null);
+
+ const calculateCombinedProfileMemo = React.useCallback(
+ (doseSchedule, deviationList = [], correction = null) =>
+ calculateCombinedProfile(
+ doseSchedule,
+ deviationList,
+ correction,
+ steadyStateConfig,
+ simulationDays,
+ pkParams
+ ),
+ [steadyStateConfig, simulationDays, pkParams]
+ );
+
+ const generateSuggestionMemo = React.useCallback(() => {
+ const newSuggestion = generateSuggestion(
+ doses,
+ deviations,
+ doseIncrement,
+ simulationDays,
+ steadyStateConfig,
+ pkParams
+ );
+ setSuggestion(newSuggestion);
+ }, [doses, deviations, doseIncrement, simulationDays, steadyStateConfig, pkParams]);
+
+ React.useEffect(() => {
+ generateSuggestionMemo();
+ }, [generateSuggestionMemo]);
+
+ const idealProfile = React.useMemo(() =>
+ calculateCombinedProfileMemo(doses),
+ [doses, calculateCombinedProfileMemo]
+ );
+
+ const deviatedProfile = React.useMemo(() =>
+ deviations.length > 0 ? calculateCombinedProfileMemo(doses, deviations) : null,
+ [doses, deviations, calculateCombinedProfileMemo]
+ );
+
+ const correctedProfile = React.useMemo(() =>
+ suggestion && suggestion.dose ? calculateCombinedProfileMemo(doses, deviations, suggestion) : null,
+ [doses, deviations, suggestion, calculateCombinedProfileMemo]
+ );
+
+ const addDeviation = () => {
+ const sortedDoses = [...doses].sort((a,b) => timeToMinutes(a.time) - timeToMinutes(b.time));
+ let nextDose = sortedDoses[0] || { time: '08:00', dose: '25' };
+ if (deviations.length > 0) {
+ const lastDev = deviations[deviations.length - 1];
+ const lastDevTime = timeToMinutes(lastDev.time) + (lastDev.dayOffset || 0) * 24 * 60;
+ const nextPlanned = sortedDoses.find(d => timeToMinutes(d.time) > (lastDevTime % (24*60)));
+ if (nextPlanned) {
+ nextDose = { ...nextPlanned, dayOffset: lastDev.dayOffset };
+ } else {
+ nextDose = { ...sortedDoses[0], dayOffset: (lastDev.dayOffset || 0) + 1 };
+ }
+ }
+ setDeviations([...deviations, { ...nextDose, isAdditional: false, dayOffset: nextDose.dayOffset || 0 }]);
+ };
+
+ const removeDeviation = (index) => {
+ setDeviations(deviations.filter((_, i) => i !== index));
+ };
+
+ const handleDeviationChange = (index, field, value) => {
+ const newDeviations = [...deviations];
+ newDeviations[index][field] = value;
+ setDeviations(newDeviations);
+ };
+
+ const applySuggestion = () => {
+ if (!suggestion || !suggestion.dose) return;
+ setDeviations([...deviations, suggestion]);
+ setSuggestion(null);
+ };
+
+ return {
+ deviations,
+ suggestion,
+ idealProfile,
+ deviatedProfile,
+ correctedProfile,
+ addDeviation,
+ removeDeviation,
+ handleDeviationChange,
+ applySuggestion
+ };
+};
diff --git a/src/utils/calculations.js b/src/utils/calculations.js
new file mode 100644
index 0000000..ed9a491
--- /dev/null
+++ b/src/utils/calculations.js
@@ -0,0 +1,65 @@
+import { timeToMinutes } from './timeUtils.js';
+import { calculateSingleDoseConcentration } from './pharmacokinetics.js';
+
+export const calculateCombinedProfile = (
+ doseSchedule,
+ deviationList = [],
+ correction = null,
+ steadyStateConfig,
+ simulationDays,
+ pkParams
+) => {
+ const dataPoints = [];
+ const timeStepHours = 0.25;
+ const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
+ const daysToSimulate = Math.min(parseInt(steadyStateConfig.daysOnMedication, 10) || 0, 5);
+
+ for (let t = 0; t <= totalHours; t += timeStepHours) {
+ let totalLdx = 0;
+ let totalDamph = 0;
+ let allDoses = [];
+
+ const maxDayOffset = (parseInt(simulationDays, 10) || 3) - 1;
+
+ for (let day = -daysToSimulate; day <= maxDayOffset; day++) {
+ const dayOffset = day * 24 * 60;
+ doseSchedule.forEach(d => {
+ allDoses.push({ ...d, time: timeToMinutes(d.time) + dayOffset, isPlan: true });
+ });
+ }
+
+ const currentDeviations = [...deviationList];
+ if (correction) {
+ currentDeviations.push({ ...correction, isAdditional: true });
+ }
+
+ currentDeviations.forEach(dev => {
+ const devTime = timeToMinutes(dev.time) + (dev.dayOffset || 0) * 24 * 60;
+ if (!dev.isAdditional) {
+ const closestDoseIndex = allDoses.reduce((closest, dose, index) => {
+ if (!dose.isPlan) return closest;
+ const diff = Math.abs(dose.time - devTime);
+ if (diff <= 60 && diff < closest.minDiff) {
+ return { index, minDiff: diff };
+ }
+ return closest;
+ }, { index: -1, minDiff: 61 }).index;
+ if (closestDoseIndex !== -1) {
+ allDoses.splice(closestDoseIndex, 1);
+ }
+ }
+ allDoses.push({ ...dev, time: devTime });
+ });
+
+ allDoses.forEach(doseInfo => {
+ const timeSinceDoseHours = t - doseInfo.time / 60;
+ const concentrations = calculateSingleDoseConcentration(doseInfo.dose, timeSinceDoseHours, pkParams);
+ totalLdx += concentrations.ldx;
+ totalDamph += concentrations.damph;
+ });
+
+ dataPoints.push({ timeHours: t, ldx: totalLdx, damph: totalDamph });
+ }
+
+ return dataPoints;
+};
diff --git a/src/utils/pharmacokinetics.js b/src/utils/pharmacokinetics.js
new file mode 100644
index 0000000..ca4fdcc
--- /dev/null
+++ b/src/utils/pharmacokinetics.js
@@ -0,0 +1,29 @@
+import { LDX_TO_DAMPH_CONVERSION_FACTOR } from '../constants/defaults.js';
+
+// Pharmacokinetic calculations
+export const calculateSingleDoseConcentration = (dose, timeSinceDoseHours, pkParams) => {
+ const numDose = parseFloat(dose) || 0;
+ if (timeSinceDoseHours < 0 || numDose <= 0) return { ldx: 0, damph: 0 };
+
+ const ka_ldx = Math.log(2) / (parseFloat(pkParams.ldx.absorptionRate) || 1);
+ const k_conv = Math.log(2) / (parseFloat(pkParams.ldx.halfLife) || 1);
+ const ke_damph = Math.log(2) / (parseFloat(pkParams.damph.halfLife) || 1);
+
+ let ldxConcentration = 0;
+ if (Math.abs(ka_ldx - k_conv) > 0.0001) {
+ ldxConcentration = (numDose * ka_ldx / (ka_ldx - k_conv)) *
+ (Math.exp(-k_conv * timeSinceDoseHours) - Math.exp(-ka_ldx * timeSinceDoseHours));
+ }
+
+ let damphConcentration = 0;
+ if (Math.abs(ka_ldx - ke_damph) > 0.0001 &&
+ Math.abs(k_conv - ke_damph) > 0.0001 &&
+ Math.abs(ka_ldx - k_conv) > 0.0001) {
+ const term1 = Math.exp(-ke_damph * timeSinceDoseHours) / ((ka_ldx - ke_damph) * (k_conv - ke_damph));
+ const term2 = Math.exp(-k_conv * timeSinceDoseHours) / ((ka_ldx - k_conv) * (ke_damph - k_conv));
+ const term3 = Math.exp(-ka_ldx * timeSinceDoseHours) / ((k_conv - ka_ldx) * (ke_damph - ka_ldx));
+ damphConcentration = LDX_TO_DAMPH_CONVERSION_FACTOR * numDose * ka_ldx * k_conv * (term1 + term2 + term3);
+ }
+
+ return { ldx: Math.max(0, ldxConcentration), damph: Math.max(0, damphConcentration) };
+};
diff --git a/src/utils/suggestions.js b/src/utils/suggestions.js
new file mode 100644
index 0000000..9f6c2cb
--- /dev/null
+++ b/src/utils/suggestions.js
@@ -0,0 +1,69 @@
+import { timeToMinutes } from './timeUtils.js';
+import { calculateCombinedProfile } from './calculations.js';
+
+export const generateSuggestion = (
+ doses,
+ deviations,
+ doseIncrement,
+ simulationDays,
+ steadyStateConfig,
+ pkParams
+) => {
+ if (deviations.length === 0) {
+ return null;
+ }
+
+ const lastDeviation = [...deviations].sort((a, b) =>
+ timeToMinutes(a.time) + (a.dayOffset || 0) * 1440 -
+ (timeToMinutes(b.time) + (b.dayOffset || 0) * 1440)
+ ).pop();
+
+ const deviationTimeTotalMinutes = timeToMinutes(lastDeviation.time) + (lastDeviation.dayOffset || 0) * 1440;
+
+ let nextDose = null;
+ let minDiff = Infinity;
+
+ doses.forEach(d => {
+ const doseTimeInMinutes = timeToMinutes(d.time);
+ for (let i = 0; i < (parseInt(simulationDays, 10) || 1); i++) {
+ const absoluteTime = doseTimeInMinutes + i * 1440;
+ const diff = absoluteTime - deviationTimeTotalMinutes;
+ if (diff > 0 && diff < minDiff) {
+ minDiff = diff;
+ nextDose = { ...d, dayOffset: i };
+ }
+ }
+ });
+
+ if (!nextDose) {
+ return { text: "Keine passende nächste Dosis für Korrektur gefunden." };
+ }
+
+ const numDoseIncrement = parseFloat(doseIncrement) || 1;
+ const idealProfile = calculateCombinedProfile(doses, [], null, steadyStateConfig, simulationDays, pkParams);
+ const deviatedProfile = calculateCombinedProfile(doses, deviations, null, steadyStateConfig, simulationDays, pkParams);
+
+ const nextDoseTimeHours = (timeToMinutes(nextDose.time) + (nextDose.dayOffset || 0) * 1440) / 60;
+
+ const idealConcentration = idealProfile.find(p => Math.abs(p.timeHours - nextDoseTimeHours) < 0.1)?.damph || 0;
+ const deviatedConcentration = deviatedProfile.find(p => Math.abs(p.timeHours - nextDoseTimeHours) < 0.1)?.damph || 0;
+ const concentrationDifference = idealConcentration - deviatedConcentration;
+
+ if (Math.abs(concentrationDifference) < 0.5) {
+ return { text: "Keine signifikante Korrektur notwendig." };
+ }
+
+ const doseAdjustmentFactor = 0.5;
+ let doseChange = concentrationDifference / doseAdjustmentFactor;
+ doseChange = Math.round(doseChange / numDoseIncrement) * numDoseIncrement;
+ let suggestedDoseValue = (parseFloat(nextDose.dose) || 0) + doseChange;
+ suggestedDoseValue = Math.max(0, Math.min(70, suggestedDoseValue));
+
+ return {
+ time: nextDose.time,
+ dose: String(suggestedDoseValue),
+ isAdditional: false,
+ originalDose: nextDose.dose,
+ dayOffset: nextDose.dayOffset
+ };
+};
diff --git a/src/utils/timeUtils.js b/src/utils/timeUtils.js
new file mode 100644
index 0000000..62a0969
--- /dev/null
+++ b/src/utils/timeUtils.js
@@ -0,0 +1,6 @@
+// --- Helper Functions ---
+export const timeToMinutes = (timeStr) => {
+ if (!timeStr || !timeStr.includes(':')) return 0;
+ const [hours, minutes] = timeStr.split(':').map(Number);
+ return hours * 60 + minutes;
+};