Update style improvements and minor fixes

This commit is contained in:
2025-11-29 18:45:30 +00:00
parent 19770a2a3c
commit 5865d9dbe5
7 changed files with 162 additions and 86 deletions

View File

@@ -64,7 +64,7 @@ const MedPlanAssistant = () => {
removeDeviation, removeDeviation,
handleDeviationChange, handleDeviationChange,
applySuggestion applySuggestion
} = useSimulation(appState); } = useSimulation(appState, t);
return ( return (
<div className="min-h-screen bg-background p-4 sm:p-6 lg:p-8"> <div className="min-h-screen bg-background p-4 sm:p-6 lg:p-8">

View File

@@ -36,69 +36,71 @@ const DeviationList = ({
</CardHeader> </CardHeader>
<CardContent className="space-y-2"> <CardContent className="space-y-2">
{deviations.map((dev: any, index: number) => ( {deviations.map((dev: any, index: number) => (
<div key={index} className="flex items-center gap-3 p-3 bg-card rounded-lg border flex-wrap"> <div key={index} className="relative flex items-start gap-3 p-3 bg-card rounded-lg border flex-wrap">
<Select <div className="flex items-center gap-3 flex-1 flex-wrap">
value={String(dev.dayOffset || 0)} <Select
onValueChange={val => onDeviationChange(index, 'dayOffset', parseInt(val, 10))} value={String(dev.dayOffset || 0)}
> onValueChange={val => onDeviationChange(index, 'dayOffset', parseInt(val, 10))}
<SelectTrigger className="w-28"> >
<SelectValue /> <SelectTrigger className="w-28">
</SelectTrigger> <SelectValue />
<SelectContent> </SelectTrigger>
{[...Array(parseInt(simulationDays, 10) || 1).keys()].map(day => ( <SelectContent>
<SelectItem key={day} value={String(day)}> {[...Array(parseInt(simulationDays, 10) || 1).keys()].map(day => (
{t.day} {day + 1} <SelectItem key={day} value={String(day)}>
</SelectItem> {t.day} {day + 1}
))} </SelectItem>
</SelectContent> ))}
</Select> </SelectContent>
</Select>
<FormTimeInput <FormTimeInput
value={dev.time} value={dev.time}
onChange={newTime => onDeviationChange(index, 'time', newTime)} onChange={newTime => onDeviationChange(index, 'time', newTime)}
required={true} required={true}
errorMessage={t.timeRequired || 'Time is required'} errorMessage={t.timeRequired || 'Time is required'}
/> />
<FormNumericInput <FormNumericInput
value={dev.dose} value={dev.dose}
onChange={newDose => onDeviationChange(index, 'dose', newDose)} onChange={newDose => onDeviationChange(index, 'dose', newDose)}
increment={doseIncrement} increment={doseIncrement}
min={0} min={0}
unit={t.mg} unit={t.mg}
required={true} required={true}
errorMessage={t.doseRequired || 'Dose is required'} errorMessage={t.doseRequired || 'Dose is required'}
/> />
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
<Switch
id={`add_dose_${index}`}
checked={dev.isAdditional}
onCheckedChange={checked => onDeviationChange(index, 'isAdditional', checked)}
/>
<Label htmlFor={`add_dose_${index}`} className="text-xs whitespace-nowrap">
{t.additional}
</Label>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t.additionalTooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Button <Button
type="button" type="button"
variant="ghost" variant="outline"
size="icon" size="icon"
onClick={() => onRemoveDeviation(index)} onClick={() => onRemoveDeviation(index)}
className="text-destructive hover:text-destructive" className="absolute top-3 right-3 text-destructive hover:bg-destructive hover:text-destructive-foreground border-destructive/30"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
<Switch
id={`add_dose_${index}`}
checked={dev.isAdditional}
onCheckedChange={checked => onDeviationChange(index, 'isAdditional', checked)}
/>
<Label htmlFor={`add_dose_${index}`} className="text-xs whitespace-nowrap">
{t.additional}
</Label>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t.additionalTooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
))} ))}

View File

@@ -35,7 +35,7 @@ const Settings = ({
<CardTitle>{t.advancedSettings}</CardTitle> <CardTitle>{t.advancedSettings}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center gap-3">
<Label htmlFor="showDayTimeOnXAxis" className="font-medium"> <Label htmlFor="showDayTimeOnXAxis" className="font-medium">
{t.show24hTimeAxis} {t.show24hTimeAxis}
</Label> </Label>

View File

@@ -12,6 +12,27 @@
import React from 'react'; import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer } from 'recharts'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer } from 'recharts';
// Chart color scheme
const CHART_COLORS = {
// d-Amphetamine profiles
idealDamph: '#2563eb', // blue-600 (primary, solid, bold)
deviatedDamph: '#f59e0b', // amber-500 (warning, dashed)
correctedDamph: '#10b981', // emerald-500 (success, dash-dot)
// Lisdexamfetamine profiles
idealLdx: '#7c3aed', // violet-600 (primary, dashed)
deviatedLdx: '#f97316', // orange-500 (warning, dashed)
correctedLdx: '#059669', // emerald-600 (success, dash-dot)
// Reference lines
therapeuticMin: '#22c55e', // green-500
therapeuticMax: '#ef4444', // red-500
dayDivider: '#9ca3af', // gray-400
// Tooltip cursor
cursor: '#6b7280' // gray-500
} as const;
const SimulationChart = ({ const SimulationChart = ({
idealProfile, idealProfile,
deviatedProfile, deviatedProfile,
@@ -46,11 +67,47 @@ const SimulationChart = ({
return [domainMin, domainMax]; return [domainMin, domainMax];
}, [yAxisMin, yAxisMax]); }, [yAxisMin, yAxisMax]);
// Merge all profiles into a single dataset for proper tooltip synchronization
const mergedData = React.useMemo(() => {
const dataMap = new Map();
// Add ideal profile data
idealProfile?.forEach((point: any) => {
dataMap.set(point.timeHours, {
timeHours: point.timeHours,
idealDamph: point.damph,
idealLdx: point.ldx
});
});
// Add deviated profile data
deviatedProfile?.forEach((point: any) => {
const existing = dataMap.get(point.timeHours) || { timeHours: point.timeHours };
dataMap.set(point.timeHours, {
...existing,
deviatedDamph: point.damph,
deviatedLdx: point.ldx
});
});
// Add corrected profile data
correctedProfile?.forEach((point: any) => {
const existing = dataMap.get(point.timeHours) || { timeHours: point.timeHours };
dataMap.set(point.timeHours, {
...existing,
correctedDamph: point.damph,
correctedLdx: point.ldx
});
});
return Array.from(dataMap.values()).sort((a, b) => a.timeHours - b.timeHours);
}, [idealProfile, deviatedProfile, correctedProfile]);
return ( return (
<div className="flex-grow w-full overflow-x-auto"> <div className="flex-grow w-full overflow-x-auto overflow-y-hidden">
<div style={{ width: `${chartWidthPercentage}%`, height: '100%', minWidth: '100%' }}> <div style={{ width: `${chartWidthPercentage}%`, height: '100%', minWidth: '100%' }}>
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart margin={{ top: 20, right: 20, left: 0, bottom: 5 }}> <LineChart data={mergedData} margin={{ top: 20, right: 20, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis <XAxis
dataKey="timeHours" dataKey="timeHours"
@@ -76,14 +133,18 @@ const SimulationChart = ({
<Tooltip <Tooltip
formatter={(value: any, name) => [`${typeof value === 'number' ? value.toFixed(1) : value} ${t.ngml}`, name]} formatter={(value: any, name) => [`${typeof value === 'number' ? value.toFixed(1) : value} ${t.ngml}`, name]}
labelFormatter={(label) => `${t.hour.replace('h', 'Hour')}: ${label}${t.hour}`} labelFormatter={(label) => `${t.hour.replace('h', 'Hour')}: ${label}${t.hour}`}
wrapperStyle={{ pointerEvents: 'none', zIndex: 200 }}
allowEscapeViewBox={{ x: false, y: false }}
cursor={{ stroke: CHART_COLORS.cursor, strokeWidth: 1, strokeDasharray: '1 1' }}
position={{ y: 0 }}
/> />
<Legend verticalAlign="top" height={36} /> <Legend verticalAlign="top" align="left" height={36} wrapperStyle={{ zIndex: 100, marginLeft: 60 }} />
{(chartView === 'damph' || chartView === 'both') && ( {(chartView === 'damph' || chartView === 'both') && (
<ReferenceLine <ReferenceLine
y={parseFloat(therapeuticRange.min) || 0} y={parseFloat(therapeuticRange.min) || 0}
label={{ value: t.min, position: 'insideTopLeft' }} label={{ value: t.min, position: 'insideTopLeft' }}
stroke="green" stroke={CHART_COLORS.therapeuticMin}
strokeDasharray="3 3" strokeDasharray="3 3"
xAxisId="hours" xAxisId="hours"
/> />
@@ -92,7 +153,7 @@ const SimulationChart = ({
<ReferenceLine <ReferenceLine
y={parseFloat(therapeuticRange.max) || 0} y={parseFloat(therapeuticRange.max) || 0}
label={{ value: t.max, position: 'insideTopLeft' }} label={{ value: t.max, position: 'insideTopLeft' }}
stroke="red" stroke={CHART_COLORS.therapeuticMax}
strokeDasharray="3 3" strokeDasharray="3 3"
xAxisId="hours" xAxisId="hours"
/> />
@@ -103,7 +164,7 @@ const SimulationChart = ({
<ReferenceLine <ReferenceLine
key={day} key={day}
x={day * 24} x={day * 24}
stroke="#999" stroke={CHART_COLORS.dayDivider}
strokeDasharray="5 5" strokeDasharray="5 5"
xAxisId="hours" xAxisId="hours"
/> />
@@ -113,80 +174,80 @@ const SimulationChart = ({
{(chartView === 'damph' || chartView === 'both') && ( {(chartView === 'damph' || chartView === 'both') && (
<Line <Line
type="monotone" type="monotone"
data={idealProfile} dataKey="idealDamph"
dataKey="damph"
name={`${t.dAmphetamine} (Ideal)`} name={`${t.dAmphetamine} (Ideal)`}
stroke="#3b82f6" stroke={CHART_COLORS.idealDamph}
strokeWidth={2.5} strokeWidth={2.5}
dot={false} dot={false}
xAxisId="hours" xAxisId="hours"
connectNulls
/> />
)} )}
{(chartView === 'ldx' || chartView === 'both') && ( {(chartView === 'ldx' || chartView === 'both') && (
<Line <Line
type="monotone" type="monotone"
data={idealProfile} dataKey="idealLdx"
dataKey="ldx"
name={`${t.lisdexamfetamine} (Ideal)`} name={`${t.lisdexamfetamine} (Ideal)`}
stroke="#8b5cf6" stroke={CHART_COLORS.idealLdx}
strokeWidth={2} strokeWidth={2}
dot={false} dot={false}
strokeDasharray="3 3" strokeDasharray="3 3"
xAxisId="hours" xAxisId="hours"
connectNulls
/> />
)} )}
{deviatedProfile && (chartView === 'damph' || chartView === 'both') && ( {deviatedProfile && (chartView === 'damph' || chartView === 'both') && (
<Line <Line
type="monotone" type="monotone"
data={deviatedProfile} dataKey="deviatedDamph"
dataKey="damph"
name={`${t.dAmphetamine} (Deviation)`} name={`${t.dAmphetamine} (Deviation)`}
stroke="#f59e0b" stroke={CHART_COLORS.deviatedDamph}
strokeWidth={2} strokeWidth={2}
strokeDasharray="5 5" strokeDasharray="5 5"
dot={false} dot={false}
xAxisId="hours" xAxisId="hours"
connectNulls
/> />
)} )}
{deviatedProfile && (chartView === 'ldx' || chartView === 'both') && ( {deviatedProfile && (chartView === 'ldx' || chartView === 'both') && (
<Line <Line
type="monotone" type="monotone"
data={deviatedProfile} dataKey="deviatedLdx"
dataKey="ldx"
name={`${t.lisdexamfetamine} (Deviation)`} name={`${t.lisdexamfetamine} (Deviation)`}
stroke="#f97316" stroke={CHART_COLORS.deviatedLdx}
strokeWidth={1.5} strokeWidth={1.5}
strokeDasharray="5 5" strokeDasharray="5 5"
dot={false} dot={false}
xAxisId="hours" xAxisId="hours"
connectNulls
/> />
)} )}
{correctedProfile && (chartView === 'damph' || chartView === 'both') && ( {correctedProfile && (chartView === 'damph' || chartView === 'both') && (
<Line <Line
type="monotone" type="monotone"
data={correctedProfile} dataKey="correctedDamph"
dataKey="damph"
name={`${t.dAmphetamine} (Correction)`} name={`${t.dAmphetamine} (Correction)`}
stroke="#10b981" stroke={CHART_COLORS.correctedDamph}
strokeWidth={2.5} strokeWidth={2.5}
strokeDasharray="3 7" strokeDasharray="3 7"
dot={false} dot={false}
xAxisId="hours" xAxisId="hours"
connectNulls
/> />
)} )}
{correctedProfile && (chartView === 'ldx' || chartView === 'both') && ( {correctedProfile && (chartView === 'ldx' || chartView === 'both') && (
<Line <Line
type="monotone" type="monotone"
data={correctedProfile} dataKey="correctedLdx"
dataKey="ldx"
name={`${t.lisdexamfetamine} (Correction)`} name={`${t.lisdexamfetamine} (Correction)`}
stroke="#059669" stroke={CHART_COLORS.correctedLdx}
strokeWidth={2} strokeWidth={2}
strokeDasharray="3 7" strokeDasharray="3 7"
dot={false} dot={false}
xAxisId="hours" xAxisId="hours"
connectNulls
/> />
)} )}
</LineChart> </LineChart>

View File

@@ -136,7 +136,7 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
return ( return (
<div ref={containerRef} className={cn("relative flex items-center gap-2", className)}> <div ref={containerRef} className={cn("relative flex items-center gap-2", className)}>
<div className="flex items-center flex-1"> <div className="flex items-center">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@@ -159,6 +159,7 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
onFocus={handleFocus} onFocus={handleFocus}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={cn( className={cn(
"w-24",
"rounded-none border-x-0 h-9", "rounded-none border-x-0 h-9",
getAlignmentClass(), getAlignmentClass(),
hasError && "border-destructive focus-visible:ring-destructive" hasError && "border-destructive focus-visible:ring-destructive"

View File

@@ -24,7 +24,12 @@ interface SuggestionResult {
dayOffset?: number; dayOffset?: number;
} }
export const useSimulation = (appState: AppState) => { interface Translations {
noSuitableNextDose: string;
noSignificantCorrection: string;
}
export const useSimulation = (appState: AppState, t: Translations) => {
const { pkParams, doses, steadyStateConfig, doseIncrement, uiSettings } = appState; const { pkParams, doses, steadyStateConfig, doseIncrement, uiSettings } = appState;
const { simulationDays } = uiSettings; const { simulationDays } = uiSettings;
@@ -51,10 +56,11 @@ export const useSimulation = (appState: AppState) => {
doseIncrement, doseIncrement,
simulationDays, simulationDays,
steadyStateConfig, steadyStateConfig,
pkParams pkParams,
t
); );
setSuggestion(newSuggestion); setSuggestion(newSuggestion);
}, [doses, deviations, doseIncrement, simulationDays, steadyStateConfig, pkParams]); }, [doses, deviations, doseIncrement, simulationDays, steadyStateConfig, pkParams, t]);
React.useEffect(() => { React.useEffect(() => {
generateSuggestionMemo(); generateSuggestionMemo();

View File

@@ -22,13 +22,19 @@ interface SuggestionResult {
dayOffset?: number; dayOffset?: number;
} }
interface Translations {
noSuitableNextDose: string;
noSignificantCorrection: string;
}
export const generateSuggestion = ( export const generateSuggestion = (
doses: Dose[], doses: Dose[],
deviations: Deviation[], deviations: Deviation[],
doseIncrement: string, doseIncrement: string,
simulationDays: string, simulationDays: string,
steadyStateConfig: SteadyStateConfig, steadyStateConfig: SteadyStateConfig,
pkParams: PkParams pkParams: PkParams,
t: Translations
): SuggestionResult | null => { ): SuggestionResult | null => {
if (deviations.length === 0) { if (deviations.length === 0) {
return null; return null;
@@ -68,7 +74,7 @@ export const generateSuggestion = (
}); });
if (!nextDose) { if (!nextDose) {
return { text: "Keine passende nächste Dosis für Korrektur gefunden." }; return { text: t.noSuitableNextDose };
} }
// Type assertion after null check // Type assertion after null check
@@ -85,7 +91,7 @@ export const generateSuggestion = (
const concentrationDifference = idealConcentration - deviatedConcentration; const concentrationDifference = idealConcentration - deviatedConcentration;
if (Math.abs(concentrationDifference) < 0.5) { if (Math.abs(concentrationDifference) < 0.5) {
return { text: "Keine signifikante Korrektur notwendig." }; return { text: t.noSignificantCorrection };
} }
const doseAdjustmentFactor = 0.5; const doseAdjustmentFactor = 0.5;