Compare commits
10 Commits
e398b1cb29
...
399b09d924
| Author | SHA1 | Date | |
|---|---|---|---|
| 399b09d924 | |||
| 9e268cbc1b | |||
| 6b9d8cdf49 | |||
| d64b9eabfa | |||
| abae3d54e6 | |||
| 509cb33422 | |||
| 8bd69516c4 | |||
| bb5569aada | |||
| 63d6124ce3 | |||
| 41ffce1c23 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "med-plan-assistant",
|
||||
"version": "0.1.1",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
|
||||
@@ -36,7 +36,8 @@ const MedPlanAssistant = () => {
|
||||
removeDay,
|
||||
addDoseToDay,
|
||||
removeDoseFromDay,
|
||||
updateDoseInDay
|
||||
updateDoseInDay,
|
||||
sortDosesInDay
|
||||
} = useAppState();
|
||||
|
||||
const {
|
||||
@@ -112,6 +113,7 @@ const MedPlanAssistant = () => {
|
||||
displayedDays={displayedDays}
|
||||
yAxisMin={yAxisMin}
|
||||
yAxisMax={yAxisMax}
|
||||
days={days}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
@@ -126,6 +128,7 @@ const MedPlanAssistant = () => {
|
||||
onAddDose={addDoseToDay}
|
||||
onRemoveDose={removeDoseFromDay}
|
||||
onUpdateDose={updateDoseInDay}
|
||||
onSortDoses={sortDosesInDay}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,8 @@ import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { FormTimeInput } from './ui/form-time-input';
|
||||
import { FormNumericInput } from './ui/form-numeric-input';
|
||||
import { Plus, Copy, Trash2 } from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
|
||||
import { Plus, Copy, Trash2, ArrowDownAZ, ChevronDown, ChevronUp, TrendingUp, TrendingDown } from 'lucide-react';
|
||||
import type { DayGroup } from '../constants/defaults';
|
||||
|
||||
interface DayScheduleProps {
|
||||
@@ -25,6 +26,7 @@ interface DayScheduleProps {
|
||||
onAddDose: (dayId: string) => void;
|
||||
onRemoveDose: (dayId: string, doseId: string) => void;
|
||||
onUpdateDose: (dayId: string, doseId: string, field: 'time' | 'ldx' | 'damph', value: string) => void;
|
||||
onSortDoses: (dayId: string) => void;
|
||||
t: any;
|
||||
}
|
||||
|
||||
@@ -36,23 +38,128 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
||||
onAddDose,
|
||||
onRemoveDose,
|
||||
onUpdateDose,
|
||||
onSortDoses,
|
||||
t
|
||||
}) => {
|
||||
const canAddDay = days.length < 3;
|
||||
|
||||
// Track collapsed state for each day (by day ID)
|
||||
const [collapsedDays, setCollapsedDays] = React.useState<Set<string>>(new Set());
|
||||
|
||||
const toggleDayCollapse = (dayId: string) => {
|
||||
setCollapsedDays(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(dayId)) {
|
||||
newSet.delete(dayId);
|
||||
} else {
|
||||
newSet.add(dayId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// Check if doses are sorted chronologically
|
||||
const isDaySorted = (day: DayGroup): boolean => {
|
||||
for (let i = 1; i < day.doses.length; i++) {
|
||||
const prevTime = day.doses[i - 1].time || '00:00';
|
||||
const currTime = day.doses[i].time || '00:00';
|
||||
if (prevTime > currTime) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{days.map((day, dayIndex) => (
|
||||
{days.map((day, dayIndex) => {
|
||||
// Get template day for comparison
|
||||
const templateDay = days.find(d => d.isTemplate);
|
||||
|
||||
// Calculate differences for deviation days
|
||||
let doseCountDiff = 0;
|
||||
let totalMgDiff = 0;
|
||||
|
||||
if (!day.isTemplate && templateDay) {
|
||||
doseCountDiff = day.doses.length - templateDay.doses.length;
|
||||
const dayTotal = day.doses.reduce((sum, dose) => sum + (parseFloat(dose.ldx) || 0), 0);
|
||||
const templateTotal = templateDay.doses.reduce((sum, dose) => sum + (parseFloat(dose.ldx) || 0), 0);
|
||||
totalMgDiff = dayTotal - templateTotal;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card key={day.id}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => toggleDayCollapse(day.id)}
|
||||
title={collapsedDays.has(day.id) ? t('expandDay') : t('collapseDay')}
|
||||
>
|
||||
{collapsedDays.has(day.id) ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<CardTitle className="text-lg">
|
||||
{day.isTemplate ? t('regularPlan') : t('deviatingPlan')}
|
||||
{day.isTemplate ? t('regularPlan') : t('alternativePlan')}
|
||||
</CardTitle>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t('day')} {dayIndex + 1}
|
||||
</Badge>
|
||||
{!day.isTemplate && doseCountDiff !== 0 ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs ${doseCountDiff > 0 ? 'bg-blue-50' : 'bg-orange-50'}`}
|
||||
>
|
||||
{doseCountDiff > 0 ? <TrendingUp className="h-3 w-3 inline mr-1" /> : <TrendingDown className="h-3 w-3 inline mr-1" />}
|
||||
{day.doses.length} {day.doses.length === 1 ? t('dose') : t('doses')}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">
|
||||
{doseCountDiff > 0 ? '+' : ''}{doseCountDiff} {Math.abs(doseCountDiff) === 1 ? t('dose') : t('doses')} {t('comparedToRegularPlan')}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{day.doses.length} {day.doses.length === 1 ? t('dose') : t('doses')}
|
||||
</Badge>
|
||||
)}
|
||||
{!day.isTemplate && Math.abs(totalMgDiff) > 0.1 ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs ${totalMgDiff > 0 ? 'bg-blue-50' : 'bg-orange-50'}`}
|
||||
>
|
||||
{totalMgDiff > 0 ? <TrendingUp className="h-3 w-3 inline mr-1" /> : <TrendingDown className="h-3 w-3 inline mr-1" />}
|
||||
{day.doses.reduce((sum, dose) => sum + (parseFloat(dose.ldx) || 0), 0).toFixed(1)} mg
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">
|
||||
{totalMgDiff > 0 ? '+' : ''}{totalMgDiff.toFixed(1)} mg {t('comparedToRegularPlan')}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{day.doses.reduce((sum, dose) => sum + (parseFloat(dose.ldx) || 0), 0).toFixed(1)} mg
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{canAddDay && (
|
||||
@@ -79,10 +186,38 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{!collapsedDays.has(day.id) && (
|
||||
<CardContent className="space-y-3">
|
||||
{/* Dose table header */}
|
||||
<div className="grid grid-cols-[120px_1fr_auto] gap-3 text-sm font-medium text-muted-foreground">
|
||||
<div>{t('time')}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{t('time')}</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className={
|
||||
isDaySorted(day)
|
||||
? "h-6 w-6 p-0 text-muted-foreground hover:text-muted-foreground cursor-default"
|
||||
: "h-6 w-6 p-0 text-primary hover:text-primary hover:bg-primary/10"
|
||||
}
|
||||
onClick={() => !isDaySorted(day) && onSortDoses(day.id)}
|
||||
disabled={isDaySorted(day)}
|
||||
>
|
||||
<ArrowDownAZ className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">
|
||||
{isDaySorted(day) ? t('sortByTimeSorted') : t('sortByTimeNeeded')}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div>{t('ldx')} (mg)</div>
|
||||
<div></div>
|
||||
</div>
|
||||
@@ -93,6 +228,9 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
||||
const duplicateTimeCount = day.doses.filter(d => d.time === dose.time).length;
|
||||
const hasDuplicateTime = duplicateTimeCount > 1;
|
||||
|
||||
// Check for zero dose
|
||||
const isZeroDose = dose.ldx === '0' || dose.ldx === '0.0';
|
||||
|
||||
return (
|
||||
<div key={dose.id} className="grid grid-cols-[120px_1fr_auto] gap-3 items-center">
|
||||
<FormTimeInput
|
||||
@@ -110,7 +248,9 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
||||
min={0}
|
||||
unit="mg"
|
||||
required={true}
|
||||
warning={isZeroDose}
|
||||
errorMessage={t('errorNumberRequired')}
|
||||
warningMessage={t('warningZeroDose')}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => onRemoveDose(day.id, dose.id)}
|
||||
@@ -139,8 +279,9 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
)})}
|
||||
|
||||
{/* Add day button */}
|
||||
{canAddDay && (
|
||||
|
||||
@@ -114,7 +114,7 @@ const Settings = ({
|
||||
increment={1}
|
||||
min={3}
|
||||
max={7}
|
||||
unit={t('days')}
|
||||
unit={t('unitDays')}
|
||||
required={true}
|
||||
errorMessage={t('errorNumberRequired')}
|
||||
/>
|
||||
@@ -128,7 +128,7 @@ const Settings = ({
|
||||
increment={1}
|
||||
min={1}
|
||||
max={parseInt(simulationDays, 10) || 3}
|
||||
unit={t('days')}
|
||||
unit={t('unitDays')}
|
||||
required={true}
|
||||
errorMessage={t('errorNumberRequired')}
|
||||
/>
|
||||
@@ -140,7 +140,7 @@ const Settings = ({
|
||||
<FormNumericInput
|
||||
value={yAxisMin}
|
||||
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
||||
increment={5}
|
||||
increment={1}
|
||||
min={0}
|
||||
placeholder={t('auto')}
|
||||
allowEmpty={true}
|
||||
|
||||
@@ -46,6 +46,7 @@ const SimulationChart = ({
|
||||
displayedDays,
|
||||
yAxisMin,
|
||||
yAxisMax,
|
||||
days,
|
||||
t
|
||||
}: any) => {
|
||||
const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
|
||||
@@ -79,6 +80,62 @@ const SimulationChart = ({
|
||||
return [domainMin, domainMax];
|
||||
}, [yAxisMin, yAxisMax]);
|
||||
|
||||
// Check which days have deviations (differ from template)
|
||||
const daysWithDeviations = React.useMemo(() => {
|
||||
if (!templateProfile || !combinedProfile) return new Set<number>();
|
||||
|
||||
const deviatingDays = new Set<number>();
|
||||
const simDays = parseInt(simulationDays, 10) || 3;
|
||||
|
||||
// Check each day starting from day 2 (day 1 is always template)
|
||||
for (let day = 2; day <= simDays; day++) {
|
||||
const dayStartHour = (day - 1) * 24;
|
||||
const dayEndHour = day * 24;
|
||||
|
||||
// Sample points in this day to check for differences
|
||||
// Check every hour in the day
|
||||
for (let hour = dayStartHour; hour < dayEndHour; hour++) {
|
||||
const combinedPoint = combinedProfile.find((p: any) => Math.abs(p.timeHours - hour) < 0.1);
|
||||
const templatePoint = templateProfile.find((p: any) => Math.abs(p.timeHours - hour) < 0.1);
|
||||
|
||||
if (combinedPoint && templatePoint) {
|
||||
// Consider it different if values differ by more than 0.01 (tolerance for floating point)
|
||||
const damphDiff = Math.abs(combinedPoint.damph - templatePoint.damph);
|
||||
const ldxDiff = Math.abs(combinedPoint.ldx - templatePoint.ldx);
|
||||
|
||||
if (damphDiff > 0.01 || ldxDiff > 0.01) {
|
||||
deviatingDays.add(day);
|
||||
break; // Found deviation in this day, no need to check more hours
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deviatingDays;
|
||||
}, [combinedProfile, templateProfile, simulationDays]);
|
||||
|
||||
// Determine label for each day's reference line
|
||||
const getDayLabel = React.useCallback((dayNumber: number) => {
|
||||
if (dayNumber === 1) return t('refLineRegularPlan');
|
||||
|
||||
// Check if this day has an actual schedule entry (not auto-filled)
|
||||
const hasSchedule = days && days.length >= dayNumber;
|
||||
|
||||
// Check if this day deviates from template
|
||||
const hasDeviation = daysWithDeviations.has(dayNumber);
|
||||
|
||||
if (!hasDeviation) {
|
||||
// Matches template
|
||||
return t('refLineNoDeviation');
|
||||
} else if (!hasSchedule) {
|
||||
// Deviates but no schedule = recovering
|
||||
return t('refLineRecovering');
|
||||
} else {
|
||||
// Has deviation and has schedule = actual irregular intake
|
||||
return t('refLineIrregularIntake');
|
||||
}
|
||||
}, [days, daysWithDeviations, t]);
|
||||
|
||||
// Merge all profiles into a single dataset for proper tooltip synchronization
|
||||
const mergedData = React.useMemo(() => {
|
||||
const dataMap = new Map();
|
||||
@@ -93,17 +150,23 @@ const SimulationChart = ({
|
||||
});
|
||||
|
||||
// Add template profile data (regular plan only) if provided
|
||||
// Only include points for days that have deviations
|
||||
templateProfile?.forEach((point: any) => {
|
||||
const existing = dataMap.get(point.timeHours) || { timeHours: point.timeHours };
|
||||
dataMap.set(point.timeHours, {
|
||||
...existing,
|
||||
templateDamph: point.damph,
|
||||
templateLdx: point.ldx
|
||||
});
|
||||
const pointDay = Math.ceil(point.timeHours / 24);
|
||||
|
||||
// Only include template data for days with deviations
|
||||
if (daysWithDeviations.has(pointDay)) {
|
||||
const existing = dataMap.get(point.timeHours) || { timeHours: point.timeHours };
|
||||
dataMap.set(point.timeHours, {
|
||||
...existing,
|
||||
templateDamph: point.damph,
|
||||
templateLdx: point.ldx
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(dataMap.values()).sort((a, b) => a.timeHours - b.timeHours);
|
||||
}, [combinedProfile, templateProfile]);
|
||||
}, [combinedProfile, templateProfile, daysWithDeviations]);
|
||||
|
||||
// Calculate chart dimensions
|
||||
const [containerWidth, setContainerWidth] = React.useState(1000);
|
||||
@@ -143,6 +206,11 @@ const SimulationChart = ({
|
||||
align="left"
|
||||
height={36}
|
||||
wrapperStyle={{ paddingLeft: 0 }}
|
||||
formatter={(value: string) => {
|
||||
// Apply lighter color to template overlay entries in legend
|
||||
const isTemplate = value.includes(t('regularPlanOverlay'));
|
||||
return <span style={{ opacity: isTemplate ? 0.5 : 1 }}>{value}</span>;
|
||||
}}
|
||||
/>
|
||||
{/* Invisible lines just to show in legend */}
|
||||
{(chartView === 'damph' || chartView === 'both') && (
|
||||
@@ -166,7 +234,7 @@ const SimulationChart = ({
|
||||
strokeOpacity={0}
|
||||
/>
|
||||
)}
|
||||
{templateProfile && (chartView === 'damph' || chartView === 'both') && (
|
||||
{templateProfile && daysWithDeviations.size > 0 && (chartView === 'damph' || chartView === 'both') && (
|
||||
<Line
|
||||
dataKey="templateDamph"
|
||||
name={`${t('dAmphetamine')} (${t('regularPlanOverlay')})`}
|
||||
@@ -175,9 +243,10 @@ const SimulationChart = ({
|
||||
strokeDasharray="3 3"
|
||||
dot={false}
|
||||
strokeOpacity={0}
|
||||
opacity={0.5}
|
||||
/>
|
||||
)}
|
||||
{templateProfile && (chartView === 'ldx' || chartView === 'both') && (
|
||||
{templateProfile && daysWithDeviations.size > 0 && (chartView === 'ldx' || chartView === 'both') && (
|
||||
<Line
|
||||
dataKey="templateLdx"
|
||||
name={`${t('lisdexamfetamine')} (${t('regularPlanOverlay')})`}
|
||||
@@ -186,6 +255,7 @@ const SimulationChart = ({
|
||||
strokeDasharray="3 3"
|
||||
dot={false}
|
||||
strokeOpacity={0}
|
||||
opacity={0.5}
|
||||
/>
|
||||
)}
|
||||
</LineChart>
|
||||
@@ -235,7 +305,7 @@ const SimulationChart = ({
|
||||
};
|
||||
return <XAxis
|
||||
xAxisId="hours"
|
||||
label={{ value: showDayTimeOnXAxis === 'continuous' ? t('axisLabelHours') : t('axisLabelTimeOfDay'), position: 'insideBottom', offset: -10, style: { fontStyle: 'italic', color: '#666' } }}
|
||||
//label={{ value: showDayTimeOnXAxis === 'continuous' ? t('axisLabelHours') : t('axisLabelTimeOfDay'), position: 'insideBottom', offset: -10, style: { fontStyle: 'italic', color: '#666' } }}
|
||||
dataKey="timeHours"
|
||||
type="number"
|
||||
domain={[0, totalHours]}
|
||||
@@ -248,17 +318,57 @@ const SimulationChart = ({
|
||||
|
||||
<YAxis
|
||||
yAxisId="concentration"
|
||||
label={{ value: t('axisLabelConcentration'), angle: -90, position: 'insideLeft', offset: '0 -10', style: { fontStyle: 'italic', color: '#666' } }}
|
||||
// FIXME
|
||||
//label={{ value: t('axisLabelConcentration'), angle: -90, position: 'insideLeft', style: { fontStyle: 'italic', color: '#666' } }}
|
||||
domain={chartDomain as any}
|
||||
allowDecimals={false}
|
||||
tickCount={20}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: any, name) => [`${typeof value === 'number' ? value.toFixed(1) : value} ${t('ngml')}`, name]}
|
||||
labelFormatter={(label, payload) => {
|
||||
content={({ active, payload, label }) => {
|
||||
if (!active || !payload || payload.length === 0) return null;
|
||||
|
||||
// Extract timeHours from the payload data point
|
||||
const timeHours = payload?.[0]?.payload?.timeHours ?? label;
|
||||
return `${t('hour').replace('h', 'Hour')}: ${timeHours}${t('hour')}`;
|
||||
const timeHours = payload[0]?.payload?.timeHours ?? label;
|
||||
const h = typeof timeHours === 'number' ? timeHours : parseFloat(timeHours);
|
||||
|
||||
// Format time to match x-axis format
|
||||
let timeLabel: string;
|
||||
if (showDayTimeOnXAxis === '24h') {
|
||||
timeLabel = `${h % 24}${t('unitHour')}`;
|
||||
} else if (showDayTimeOnXAxis === '12h') {
|
||||
const hour12 = h % 24;
|
||||
if (hour12 === 12) {
|
||||
timeLabel = t('tickNoon');
|
||||
} else {
|
||||
const displayHour = hour12 === 0 ? 12 : hour12 > 12 ? hour12 - 12 : hour12;
|
||||
const period = hour12 < 12 ? 'a' : 'p';
|
||||
timeLabel = `${displayHour}${period}`;
|
||||
}
|
||||
} else {
|
||||
timeLabel = `${h}${t('unitHour')}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="recharts-default-tooltip" style={{ margin: 0, padding: 10, backgroundColor: 'rgb(255, 255, 255)', border: '1px solid rgb(204, 204, 204)', whiteSpace: 'nowrap' }}>
|
||||
<p className="recharts-tooltip-label" style={{ margin: 0 }}>{t('time')}: {timeLabel}</p>
|
||||
<ul className="recharts-tooltip-item-list" style={{ padding: 0, margin: 0 }}>
|
||||
{payload.map((entry: any, index: number) => {
|
||||
const isTemplate = entry.name?.includes(t('regularPlanOverlay'));
|
||||
const opacity = isTemplate ? 0.5 : 1;
|
||||
const value = typeof entry.value === 'number' ? entry.value.toFixed(1) : entry.value;
|
||||
|
||||
return (
|
||||
<li key={`item-${index}`} className="recharts-tooltip-item" style={{ display: 'block', paddingTop: 4, paddingBottom: 4, color: entry.color, opacity }}>
|
||||
<span className="recharts-tooltip-item-name">{entry.name}</span>
|
||||
<span className="recharts-tooltip-item-separator">: </span>
|
||||
<span className="recharts-tooltip-item-value">{value} {t('unitNgml')}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
wrapperStyle={{ pointerEvents: 'none', zIndex: 200 }}
|
||||
allowEscapeViewBox={{ x: false, y: false }}
|
||||
@@ -267,12 +377,12 @@ const SimulationChart = ({
|
||||
/>
|
||||
<CartesianGrid strokeDasharray="1 1" xAxisId="hours" yAxisId="concentration" />
|
||||
|
||||
{showDayReferenceLines !== false && [...Array(dispDays).keys()].map(day => (
|
||||
{showDayReferenceLines !== false && [...Array(dispDays+1).keys()].map(day => (
|
||||
<ReferenceLine
|
||||
key={`day-${day+1}`}
|
||||
x={24 * (day+1)}
|
||||
label={{
|
||||
value: (day === 0 ? t('refLineRegularPlan') : t('refLineDeviatingPlan')) + ' (' + t('refLineDayX', { x: day+1 }) + ')',
|
||||
value: t('refLineDayX', { x: day+1 }) + '→' + getDayLabel(day + 1),
|
||||
position: 'insideTopRight',
|
||||
style: {
|
||||
fontSize: '0.75rem',
|
||||
@@ -351,7 +461,7 @@ const SimulationChart = ({
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="templateDamph"
|
||||
name={`${t('dAmphetamine')} (${t('regularPlan')} ${t('continuation')})`}
|
||||
name={`${t('dAmphetamine')} (${t('regularPlanOverlay')})`}
|
||||
stroke={CHART_COLORS.idealDamph}
|
||||
strokeWidth={2}
|
||||
strokeDasharray="3 3"
|
||||
@@ -366,7 +476,7 @@ const SimulationChart = ({
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="templateLdx"
|
||||
name={`${t('lisdexamfetamine')} (${t('regularPlan')} ${t('continuation')})`}
|
||||
name={`${t('lisdexamfetamine')} (${t('regularPlanOverlay')})`}
|
||||
stroke={CHART_COLORS.idealLdx}
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray="3 3"
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Minus, Plus, X } from "lucide-react"
|
||||
import { Button } from "./button"
|
||||
import { Input } from "./input"
|
||||
import { cn } from "../../lib/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
interface NumericInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
||||
value: string | number
|
||||
@@ -50,9 +51,11 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const [showError, setShowError] = React.useState(false)
|
||||
const [showWarning, setShowWarning] = React.useState(false)
|
||||
const { t } = useTranslation()
|
||||
const [, setShowError] = React.useState(false)
|
||||
const [, setShowWarning] = React.useState(false)
|
||||
const [touched, setTouched] = React.useState(false)
|
||||
const [isFocused, setIsFocused] = React.useState(false)
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
// Check if value is invalid (check validity regardless of touch state)
|
||||
@@ -114,24 +117,41 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
}
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value
|
||||
const inputValue = e.target.value.trim()
|
||||
setTouched(true)
|
||||
setIsFocused(false)
|
||||
setShowError(false)
|
||||
setShowWarning(false)
|
||||
|
||||
if (val === '' && !allowEmpty) {
|
||||
if (inputValue === '' && !allowEmpty) {
|
||||
// Update parent with empty value so validation works
|
||||
onChange('')
|
||||
return
|
||||
}
|
||||
|
||||
if (val !== '' && !isNaN(Number(val))) {
|
||||
onChange(formatValue(val))
|
||||
if (inputValue !== '' && !isNaN(Number(inputValue))) {
|
||||
onChange(formatValue(inputValue))
|
||||
}
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsFocused(true)
|
||||
setShowError(hasError)
|
||||
setShowWarning(hasWarning)
|
||||
}
|
||||
|
||||
// Ensure value is consistently formatted to the required decimal places
|
||||
React.useEffect(() => {
|
||||
const strVal = String(value)
|
||||
if (strVal === '') return
|
||||
const num = Number(strVal)
|
||||
if (isNaN(num)) return
|
||||
const formatted = num.toFixed(decimalPlaces)
|
||||
if (strVal !== formatted) {
|
||||
onChange(formatted)
|
||||
}
|
||||
}, [value, decimalPlaces, onChange])
|
||||
|
||||
const getAlignmentClass = () => {
|
||||
switch (align) {
|
||||
case 'left': return 'text-left'
|
||||
@@ -150,7 +170,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-r-none border-r-0",
|
||||
hasError && "border-destructive"
|
||||
hasError && "border-destructive",
|
||||
hasWarning && !hasError && "border-yellow-500"
|
||||
)}
|
||||
onClick={() => updateValue(-1)}
|
||||
tabIndex={-1}
|
||||
@@ -169,7 +190,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
"w-20 h-9 z-20",
|
||||
"rounded-none",
|
||||
getAlignmentClass(),
|
||||
hasError && "border-destructive focus-visible:ring-destructive"
|
||||
hasError && "border-destructive focus-visible:ring-destructive",
|
||||
hasWarning && !hasError && "border-yellow-500 focus-visible:ring-yellow-500"
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -180,7 +202,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
className={cn(
|
||||
"h-9 w-9",
|
||||
clearButton && allowEmpty ? "rounded-l-none rounded-r-none border-x-0" : "rounded-l-none border-l-0",
|
||||
hasError && "border-destructive"
|
||||
hasError && "border-destructive",
|
||||
hasWarning && !hasError && "border-yellow-500"
|
||||
)}
|
||||
onClick={() => updateValue(1)}
|
||||
tabIndex={-1}
|
||||
@@ -194,23 +217,25 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-9 w-9 rounded-l-none",
|
||||
hasError && "border-destructive"
|
||||
hasError && "border-destructive",
|
||||
hasWarning && !hasError && "border-yellow-500"
|
||||
)}
|
||||
onClick={() => onChange('')}
|
||||
tabIndex={-1}
|
||||
title={ t('buttonClear') }
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
||||
{hasError && showError && errorMessage && (
|
||||
{hasError && isFocused && errorMessage && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-64 bg-destructive text-destructive-foreground text-xs p-2 rounded-md shadow-lg">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
{hasWarning && showWarning && warningMessage && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-yellow-500 text-yellow-950 text-xs p-2 rounded-md shadow-lg">
|
||||
{hasWarning && isFocused && warningMessage && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-yellow-500 text-white text-xs p-2 rounded-md shadow-lg">
|
||||
{warningMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Button } from "./button"
|
||||
import { Input } from "./input"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./popover"
|
||||
import { cn } from "../../lib/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
interface TimeInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
||||
value: string
|
||||
@@ -41,13 +42,22 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
||||
className,
|
||||
...props
|
||||
}, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const [displayValue, setDisplayValue] = React.useState(value)
|
||||
const [isPickerOpen, setIsPickerOpen] = React.useState(false)
|
||||
const [showError, setShowError] = React.useState(false)
|
||||
const [showWarning, setShowWarning] = React.useState(false)
|
||||
const [, setShowError] = React.useState(false)
|
||||
const [, setShowWarning] = React.useState(false)
|
||||
const [touched, setTouched] = React.useState(false)
|
||||
const [isFocused, setIsFocused] = React.useState(false)
|
||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
// Current committed value parsed from prop
|
||||
const [pickerHours, pickerMinutes] = (value || "00:00").split(':').map(Number)
|
||||
|
||||
// Staged selections (pending confirmation)
|
||||
const [stagedHour, setStagedHour] = React.useState<number | null>(null)
|
||||
const [stagedMinute, setStagedMinute] = React.useState<number | null>(null)
|
||||
|
||||
// Check if value is invalid (check validity regardless of touch state)
|
||||
const isInvalid = required && (!value || value.trim() === '')
|
||||
const hasError = error || isInvalid
|
||||
@@ -57,9 +67,21 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
||||
setDisplayValue(value)
|
||||
}, [value])
|
||||
|
||||
// Align error bubble behavior with numeric input: show when invalid after first blur
|
||||
React.useEffect(() => {
|
||||
if (isInvalid && touched) {
|
||||
setShowError(true)
|
||||
} else if (!isInvalid) {
|
||||
setShowError(false)
|
||||
}
|
||||
}, [isInvalid, touched])
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
const inputValue = e.target.value.trim()
|
||||
setTouched(true)
|
||||
setIsFocused(false)
|
||||
setShowError(false)
|
||||
setShowWarning(false)
|
||||
|
||||
if (inputValue === '') {
|
||||
// Update parent with empty value so validation works
|
||||
@@ -98,21 +120,41 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsFocused(true)
|
||||
setShowError(hasError)
|
||||
setShowWarning(hasWarning)
|
||||
}
|
||||
|
||||
const handlePickerChange = (part: 'h' | 'm', val: number) => {
|
||||
let newHours = pickerHours, newMinutes = pickerMinutes
|
||||
if (part === 'h') {
|
||||
newHours = val
|
||||
} else {
|
||||
newMinutes = val
|
||||
const handlePickerOpen = (open: boolean) => {
|
||||
setIsPickerOpen(open)
|
||||
if (open) {
|
||||
// Reset staging when opening picker
|
||||
setStagedHour(null)
|
||||
setStagedMinute(null)
|
||||
}
|
||||
const formattedTime = `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}`
|
||||
onChange(formattedTime)
|
||||
}
|
||||
|
||||
const handleHourClick = (hour: number) => {
|
||||
setStagedHour(hour)
|
||||
}
|
||||
|
||||
const handleMinuteClick = (minute: number) => {
|
||||
setStagedMinute(minute)
|
||||
}
|
||||
|
||||
const handleApply = () => {
|
||||
// Use staged values if selected, otherwise keep current values
|
||||
const finalHour = stagedHour !== null ? stagedHour : pickerHours
|
||||
const finalMinute = stagedMinute !== null ? stagedMinute : pickerMinutes
|
||||
const formattedTime = `${String(finalHour).padStart(2, '0')}:${String(finalMinute).padStart(2, '0')}`
|
||||
onChange(formattedTime)
|
||||
setIsPickerOpen(false)
|
||||
}
|
||||
|
||||
// Apply button is enabled when both hour and minute have valid values (either staged or from current value)
|
||||
const canApply = (stagedHour !== null || pickerHours !== undefined) &&
|
||||
(stagedMinute !== null || pickerMinutes !== undefined)
|
||||
|
||||
const getAlignmentClass = () => {
|
||||
switch (align) {
|
||||
case 'left': return 'text-left'
|
||||
@@ -142,7 +184,7 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<Popover open={isPickerOpen} onOpenChange={setIsPickerOpen}>
|
||||
<Popover open={isPickerOpen} onOpenChange={handlePickerOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -158,59 +200,73 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-3 bg-popover shadow-md border">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-xs font-medium text-center mb-1">Hour</div>
|
||||
<div className="grid grid-cols-4 gap-1 max-h-60 overflow-y-auto">
|
||||
{Array.from({ length: 24 }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
type="button"
|
||||
variant={pickerHours === i ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 w-10"
|
||||
onClick={() => {
|
||||
handlePickerChange('h', i)
|
||||
setIsPickerOpen(false)
|
||||
}}
|
||||
>
|
||||
{String(i).padStart(2, '0')}
|
||||
</Button>
|
||||
))}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-xs font-medium text-center mb-1">{t('timePickerHour')}</div>
|
||||
<div className="grid grid-cols-4 gap-1 max-h-60 overflow-y-auto">
|
||||
{Array.from({ length: 24 }, (_, i) => {
|
||||
const isCurrentValue = pickerHours === i && stagedHour === null
|
||||
const isStaged = stagedHour === i
|
||||
return (
|
||||
<Button
|
||||
key={i}
|
||||
type="button"
|
||||
variant={isStaged ? "default" : isCurrentValue ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 w-10"
|
||||
onClick={() => handleHourClick(i)}
|
||||
>
|
||||
{String(i).padStart(2, '0')}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-xs font-medium text-center mb-1">{t('timePickerMinute')}</div>
|
||||
<div className="grid grid-cols-4 gap-1 max-h-60 overflow-y-auto">
|
||||
{Array.from({ length: 12 }, (_, i) => i * 5).map(minute => {
|
||||
const isCurrentValue = pickerMinutes === minute && stagedMinute === null
|
||||
const isStaged = stagedMinute === minute
|
||||
return (
|
||||
<Button
|
||||
key={minute}
|
||||
type="button"
|
||||
variant={isStaged ? "default" : isCurrentValue ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 w-10"
|
||||
onClick={() => handleMinuteClick(minute)}
|
||||
>
|
||||
{String(minute).padStart(2, '0')}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-xs font-medium text-center mb-1">Min</div>
|
||||
<div className="grid grid-cols-4 gap-1 max-h-60 overflow-y-auto">
|
||||
{Array.from({ length: 12 }, (_, i) => i * 5).map(minute => (
|
||||
<Button
|
||||
key={minute}
|
||||
type="button"
|
||||
variant={pickerMinutes === minute ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 w-10"
|
||||
onClick={() => {
|
||||
handlePickerChange('m', minute)
|
||||
setIsPickerOpen(false)
|
||||
}}
|
||||
>
|
||||
{String(minute).padStart(2, '0')}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleApply}
|
||||
disabled={!canApply}
|
||||
>
|
||||
{t('timePickerApply')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
||||
{hasError && showError && errorMessage && (
|
||||
{hasError && isFocused && errorMessage && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-destructive text-destructive-foreground text-xs p-2 rounded-md shadow-lg">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
{hasWarning && showWarning && warningMessage && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-yellow-500 text-yellow-950 text-xs p-2 rounded-md shadow-lg">
|
||||
{hasWarning && isFocused && warningMessage && (
|
||||
<div className="absolute top-full left-0 mt-1 z-50 w-48 bg-yellow-500 text-white text-xs p-2 rounded-md shadow-lg">
|
||||
{warningMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -99,12 +99,12 @@ export const getDefaultState = (): AppState => ({
|
||||
therapeuticRange: { min: '10.5', max: '11.5' },
|
||||
doseIncrement: '2.5',
|
||||
uiSettings: {
|
||||
showDayTimeOnXAxis: 'continuous',
|
||||
showTemplateDay: false,
|
||||
chartView: 'both',
|
||||
yAxisMin: '0',
|
||||
yAxisMax: '16',
|
||||
simulationDays: '3',
|
||||
displayedDays: '2',
|
||||
showDayTimeOnXAxis: '24h',
|
||||
showTemplateDay: true,
|
||||
chartView: 'damph',
|
||||
yAxisMin: '8',
|
||||
yAxisMax: '13',
|
||||
simulationDays: '5',
|
||||
displayedDays: '5',
|
||||
}
|
||||
});
|
||||
|
||||
@@ -186,20 +186,11 @@ export const useAppState = () => {
|
||||
days: prev.days.map(day => {
|
||||
if (day.id !== dayId) return day;
|
||||
|
||||
// Update the dose field
|
||||
// Update the dose field (no auto-sort)
|
||||
const updatedDoses = day.doses.map(dose =>
|
||||
dose.id === doseId ? { ...dose, [field]: value } : dose
|
||||
);
|
||||
|
||||
// Sort by time if time field was changed
|
||||
if (field === 'time') {
|
||||
updatedDoses.sort((a, b) => {
|
||||
const timeA = a.time || '00:00';
|
||||
const timeB = b.time || '00:00';
|
||||
return timeA.localeCompare(timeB);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...day,
|
||||
doses: updatedDoses
|
||||
@@ -208,6 +199,26 @@ export const useAppState = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
const sortDosesInDay = (dayId: string) => {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
days: prev.days.map(day => {
|
||||
if (day.id !== dayId) return day;
|
||||
|
||||
const sortedDoses = [...day.doses].sort((a, b) => {
|
||||
const timeA = a.time || '00:00';
|
||||
const timeB = b.time || '00:00';
|
||||
return timeA.localeCompare(timeB);
|
||||
});
|
||||
|
||||
return {
|
||||
...day,
|
||||
doses: sortedDoses
|
||||
};
|
||||
})
|
||||
}));
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -227,6 +238,7 @@ export const useAppState = () => {
|
||||
addDoseToDay,
|
||||
removeDoseFromDay,
|
||||
updateDoseInDay,
|
||||
sortDosesInDay,
|
||||
handleReset
|
||||
};
|
||||
};
|
||||
|
||||
@@ -44,10 +44,14 @@ export const de = {
|
||||
axisLabelTimeOfDay: "Tageszeit (h)",
|
||||
tickNoon: "Mittag",
|
||||
refLineRegularPlan: "Regulärer Plan",
|
||||
refLineDeviatingPlan: "Abweichung",
|
||||
refLineDeviatingPlan: "Abweichung vom Plan",
|
||||
refLineNoDeviation: "Keine Abweichung",
|
||||
refLineRecovering: "Erholung",
|
||||
refLineIrregularIntake: "Irreguläre Einnahme",
|
||||
refLineDayX: "Tag {{x}}",
|
||||
refLineMin: "Min",
|
||||
refLineMax: "Max",
|
||||
tooltipHour: "Stunde",
|
||||
|
||||
// Settings
|
||||
diagramSettings: "Diagramm-Einstellungen",
|
||||
@@ -58,10 +62,9 @@ export const de = {
|
||||
xAxisFormat24hDesc: "Wiederholender 0-24h Zyklus",
|
||||
xAxisFormat12h: "Tageszeit (12h AM/PM)",
|
||||
xAxisFormat12hDesc: "Wiederholend 12h Zyklus im AM/PM Format",
|
||||
showTemplateDayInChart: "Regulären Plan kontinuierlich im Diagramm anzeigen",
|
||||
showDayReferenceLines: "Tagestrenner anzeigen",
|
||||
showTemplateDayInChart: "Regulären Plan einblenden (nur bei abweichenden Tagen)",
|
||||
showDayReferenceLines: "Tagestrenner anzeigen (Referenzlinien und Status)",
|
||||
simulationDuration: "Simulationsdauer",
|
||||
days: "Tage",
|
||||
displayedDays: "Sichtbare Tage (im Fokus)",
|
||||
yAxisRange: "Y-Achsen-Bereich (Zoom)",
|
||||
yAxisRangeAutoButton: "A",
|
||||
@@ -70,7 +73,6 @@ export const de = {
|
||||
therapeuticRange: "Therapeutischer Bereich (Referenzlinien)",
|
||||
dAmphetamineParameters: "d-Amphetamin Parameter",
|
||||
halfLife: "Halbwertszeit",
|
||||
hours: "h",
|
||||
lisdexamfetamineParameters: "Lisdexamfetamin Parameter",
|
||||
conversionHalfLife: "Umwandlungs-Halbwertszeit",
|
||||
absorptionRate: "Absorptionsrate",
|
||||
@@ -78,8 +80,10 @@ export const de = {
|
||||
resetAllSettings: "Alle Einstellungen zurücksetzen",
|
||||
|
||||
// Units
|
||||
mg: "mg",
|
||||
ngml: "ng/ml",
|
||||
unitMg: "mg",
|
||||
unitNgml: "ng/ml",
|
||||
unitHour: "h",
|
||||
unitDays: "Tage",
|
||||
|
||||
// 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.",
|
||||
@@ -88,14 +92,19 @@ export const de = {
|
||||
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.",
|
||||
|
||||
// Number input field
|
||||
buttonClear: "Feld löschen",
|
||||
|
||||
// Field validation
|
||||
errorNumberRequired: "Bitte gib eine gültige Zahl ein.",
|
||||
errorTimeRequired: "Bitte gib eine gültige Zeitangabe ein.",
|
||||
warningDuplicateTime: "Mehrere Dosen zur gleichen Zeit.",
|
||||
warningZeroDose: "Nulldosis hat keine Auswirkung auf die Simulation.",
|
||||
|
||||
// Day-based schedule
|
||||
regularPlan: "Regulärer Plan",
|
||||
deviatingPlan: "Abweichung vom Plan",
|
||||
alternativePlan: "Alternativer Plan",
|
||||
regularPlanOverlay: "Regulär",
|
||||
dayNumber: "Tag {{number}}",
|
||||
cloneDay: "Tag klonen",
|
||||
@@ -103,6 +112,11 @@ export const de = {
|
||||
addDose: "Dosis hinzufügen",
|
||||
removeDose: "Dosis entfernen",
|
||||
removeDay: "Tag entfernen",
|
||||
collapseDay: "Tag einklappen",
|
||||
expandDay: "Tag ausklappen",
|
||||
dose: "Dosis",
|
||||
doses: "Dosen",
|
||||
comparedToRegularPlan: "verglichen mit regulärem Plan",
|
||||
time: "Zeit",
|
||||
ldx: "LDX",
|
||||
damph: "d-amph",
|
||||
@@ -112,7 +126,17 @@ export const de = {
|
||||
viewingSharedPlan: "Du siehst einen geteilten Plan",
|
||||
saveAsMyPlan: "Als meinen Plan speichern",
|
||||
discardSharedPlan: "Verwerfen",
|
||||
planCopiedToClipboard: "Plan-Link in Zwischenablage kopiert!"
|
||||
planCopiedToClipboard: "Plan-Link in Zwischenablage kopiert!",
|
||||
|
||||
// Time picker
|
||||
timePickerHour: "Stunde",
|
||||
timePickerMinute: "Minute",
|
||||
timePickerApply: "Übernehmen",
|
||||
|
||||
// Sorting
|
||||
sortByTime: "Nach Zeit sortieren",
|
||||
sortByTimeNeeded: "Dosen sind nicht in chronologischer Reihenfolge. Klicken zum Sortieren.",
|
||||
sortByTimeSorted: "Dosen sind chronologisch sortiert."
|
||||
};
|
||||
|
||||
export default de;
|
||||
|
||||
@@ -44,7 +44,10 @@ export const en = {
|
||||
axisLabelTimeOfDay: "Time of Day (h)",
|
||||
tickNoon: "Noon",
|
||||
refLineRegularPlan: "Regular Plan",
|
||||
refLineDeviatingPlan: "Deviation",
|
||||
refLineDeviatingPlan: "Deviation from Plan",
|
||||
refLineNoDeviation: "No Deviation",
|
||||
refLineRecovering: "Recovering",
|
||||
refLineIrregularIntake: "Irregular Intake",
|
||||
refLineDayX: "Day {{x}}",
|
||||
refLineMin: "Min",
|
||||
refLineMax: "Max",
|
||||
@@ -58,10 +61,9 @@ export const en = {
|
||||
xAxisFormat24hDesc: "Repeating 0-24h cycle",
|
||||
xAxisFormat12h: "Time of Day (12h AM/PM)",
|
||||
xAxisFormat12hDesc: "Repeating 12h cycle in AM/PM format",
|
||||
showTemplateDayInChart: "Overlay regular plan in chart",
|
||||
showDayReferenceLines: "Show day separators",
|
||||
showTemplateDayInChart: "Show Regular Plan (Only for Deviating Days)",
|
||||
showDayReferenceLines: "Show Day Separators (Reference Lines and Status)",
|
||||
simulationDuration: "Simulation Duration",
|
||||
days: "Days",
|
||||
displayedDays: "Visible Days (in Focus)",
|
||||
yAxisRange: "Y-Axis Range (Zoom)",
|
||||
yAxisRangeAutoButton: "A",
|
||||
@@ -70,7 +72,6 @@ export const en = {
|
||||
therapeuticRange: "Therapeutic Range (Reference Lines)",
|
||||
dAmphetamineParameters: "d-Amphetamine Parameters",
|
||||
halfLife: "Half-life",
|
||||
hours: "h",
|
||||
lisdexamfetamineParameters: "Lisdexamfetamine Parameters",
|
||||
conversionHalfLife: "Conversion Half-life",
|
||||
absorptionRate: "Absorption Rate",
|
||||
@@ -78,8 +79,10 @@ export const en = {
|
||||
resetAllSettings: "Reset All Settings",
|
||||
|
||||
// Units
|
||||
mg: "mg",
|
||||
ngml: "ng/ml",
|
||||
unitMg: "mg",
|
||||
unitNgml: "ng/ml",
|
||||
unitHour: "h",
|
||||
unitDays: "Days",
|
||||
|
||||
// Reset confirmation
|
||||
resetConfirmation: "Are you sure you want to reset all settings to default values? This cannot be undone.",
|
||||
@@ -88,14 +91,29 @@ export const en = {
|
||||
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.",
|
||||
|
||||
// Number input field
|
||||
buttonClear: "Clear field",
|
||||
|
||||
// Field validation
|
||||
errorNumberRequired: "Please enter a valid number.",
|
||||
errorTimeRequired: "Please enter a valid time.",
|
||||
warningDuplicateTime: "Multiple doses at same time.",
|
||||
warningZeroDose: "Zero dose has no effect on simulation.",
|
||||
|
||||
// Time picker
|
||||
timePickerHour: "Hour",
|
||||
timePickerMinute: "Minute",
|
||||
timePickerApply: "Apply",
|
||||
|
||||
// Sorting
|
||||
sortByTime: "Sort by time",
|
||||
sortByTimeNeeded: "Doses are not in chronological order. Click to sort.",
|
||||
sortByTimeSorted: "Doses are sorted chronologically.",
|
||||
|
||||
// Day-based schedule
|
||||
regularPlan: "Regular Plan",
|
||||
deviatingPlan: "Deviation from Plan",
|
||||
alternativePlan: "Alternative Plan",
|
||||
regularPlanOverlay: "Regular",
|
||||
dayNumber: "Day {{number}}",
|
||||
cloneDay: "Clone day",
|
||||
@@ -103,16 +121,21 @@ export const en = {
|
||||
addDose: "Add dose",
|
||||
removeDose: "Remove dose",
|
||||
removeDay: "Remove day",
|
||||
collapseDay: "Collapse day",
|
||||
expandDay: "Expand day",
|
||||
dose: "dose",
|
||||
doses: "doses",
|
||||
comparedToRegularPlan: "compared to regular plan",
|
||||
time: "Time",
|
||||
ldx: "LDX",
|
||||
damph: "d-amph",
|
||||
|
||||
// URL sharing
|
||||
sharePlan: "Share Plan",
|
||||
viewingSharedPlan: "You are viewing a shared plan",
|
||||
viewingSharedPlan: "Viewing shared plan",
|
||||
saveAsMyPlan: "Save as My Plan",
|
||||
discardSharedPlan: "Discard",
|
||||
planCopiedToClipboard: "Plan link copied to clipboard!",
|
||||
planCopiedToClipboard: "Plan link copied to clipboard!"
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
--card-foreground: 0 0% 10%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 10%;
|
||||
--primary: 0 0% 15%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 94%;
|
||||
--primary: 217 91% 60%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 220 15% 88%;
|
||||
--secondary-foreground: 0 0% 15%;
|
||||
--muted: 220 10% 95%;
|
||||
--muted-foreground: 0 0% 45%;
|
||||
|
||||
Reference in New Issue
Block a user