Update style improvements and minor fixes
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ 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">
|
||||||
|
<div className="flex items-center gap-3 flex-1 flex-wrap">
|
||||||
<Select
|
<Select
|
||||||
value={String(dev.dayOffset || 0)}
|
value={String(dev.dayOffset || 0)}
|
||||||
onValueChange={val => onDeviationChange(index, 'dayOffset', parseInt(val, 10))}
|
onValueChange={val => onDeviationChange(index, 'dayOffset', parseInt(val, 10))}
|
||||||
@@ -70,16 +71,6 @@ const DeviationList = ({
|
|||||||
errorMessage={t.doseRequired || 'Dose is required'}
|
errorMessage={t.doseRequired || 'Dose is required'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => onRemoveDeviation(index)}
|
|
||||||
className="text-destructive hover:text-destructive"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -100,6 +91,17 @@ const DeviationList = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onRemoveDeviation(index)}
|
||||||
|
className="absolute top-3 right-3 text-destructive hover:bg-destructive hover:text-destructive-foreground border-destructive/30"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user