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",
|
"name": "med-plan-assistant",
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ const MedPlanAssistant = () => {
|
|||||||
removeDay,
|
removeDay,
|
||||||
addDoseToDay,
|
addDoseToDay,
|
||||||
removeDoseFromDay,
|
removeDoseFromDay,
|
||||||
updateDoseInDay
|
updateDoseInDay,
|
||||||
|
sortDosesInDay
|
||||||
} = useAppState();
|
} = useAppState();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -112,6 +113,7 @@ const MedPlanAssistant = () => {
|
|||||||
displayedDays={displayedDays}
|
displayedDays={displayedDays}
|
||||||
yAxisMin={yAxisMin}
|
yAxisMin={yAxisMin}
|
||||||
yAxisMax={yAxisMax}
|
yAxisMax={yAxisMax}
|
||||||
|
days={days}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,6 +128,7 @@ const MedPlanAssistant = () => {
|
|||||||
onAddDose={addDoseToDay}
|
onAddDose={addDoseToDay}
|
||||||
onRemoveDose={removeDoseFromDay}
|
onRemoveDose={removeDoseFromDay}
|
||||||
onUpdateDose={updateDoseInDay}
|
onUpdateDose={updateDoseInDay}
|
||||||
|
onSortDoses={sortDosesInDay}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
|||||||
import { Badge } from './ui/badge';
|
import { Badge } from './ui/badge';
|
||||||
import { FormTimeInput } from './ui/form-time-input';
|
import { FormTimeInput } from './ui/form-time-input';
|
||||||
import { FormNumericInput } from './ui/form-numeric-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';
|
import type { DayGroup } from '../constants/defaults';
|
||||||
|
|
||||||
interface DayScheduleProps {
|
interface DayScheduleProps {
|
||||||
@@ -25,6 +26,7 @@ interface DayScheduleProps {
|
|||||||
onAddDose: (dayId: string) => void;
|
onAddDose: (dayId: string) => void;
|
||||||
onRemoveDose: (dayId: string, doseId: string) => void;
|
onRemoveDose: (dayId: string, doseId: string) => void;
|
||||||
onUpdateDose: (dayId: string, doseId: string, field: 'time' | 'ldx' | 'damph', value: string) => void;
|
onUpdateDose: (dayId: string, doseId: string, field: 'time' | 'ldx' | 'damph', value: string) => void;
|
||||||
|
onSortDoses: (dayId: string) => void;
|
||||||
t: any;
|
t: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,23 +38,128 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
onAddDose,
|
onAddDose,
|
||||||
onRemoveDose,
|
onRemoveDose,
|
||||||
onUpdateDose,
|
onUpdateDose,
|
||||||
|
onSortDoses,
|
||||||
t
|
t
|
||||||
}) => {
|
}) => {
|
||||||
const canAddDay = days.length < 3;
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<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}>
|
<Card key={day.id}>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<CardTitle className="text-lg">
|
||||||
{day.isTemplate ? t('regularPlan') : t('deviatingPlan')}
|
{day.isTemplate ? t('regularPlan') : t('alternativePlan')}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
{t('day')} {dayIndex + 1}
|
{t('day')} {dayIndex + 1}
|
||||||
</Badge>
|
</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>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{canAddDay && (
|
{canAddDay && (
|
||||||
@@ -79,10 +186,38 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
{!collapsedDays.has(day.id) && (
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="space-y-3">
|
||||||
{/* Dose table header */}
|
{/* Dose table header */}
|
||||||
<div className="grid grid-cols-[120px_1fr_auto] gap-3 text-sm font-medium text-muted-foreground">
|
<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>{t('ldx')} (mg)</div>
|
||||||
<div></div>
|
<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 duplicateTimeCount = day.doses.filter(d => d.time === dose.time).length;
|
||||||
const hasDuplicateTime = duplicateTimeCount > 1;
|
const hasDuplicateTime = duplicateTimeCount > 1;
|
||||||
|
|
||||||
|
// Check for zero dose
|
||||||
|
const isZeroDose = dose.ldx === '0' || dose.ldx === '0.0';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={dose.id} className="grid grid-cols-[120px_1fr_auto] gap-3 items-center">
|
<div key={dose.id} className="grid grid-cols-[120px_1fr_auto] gap-3 items-center">
|
||||||
<FormTimeInput
|
<FormTimeInput
|
||||||
@@ -110,7 +248,9 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
min={0}
|
min={0}
|
||||||
unit="mg"
|
unit="mg"
|
||||||
required={true}
|
required={true}
|
||||||
|
warning={isZeroDose}
|
||||||
errorMessage={t('errorNumberRequired')}
|
errorMessage={t('errorNumberRequired')}
|
||||||
|
warningMessage={t('warningZeroDose')}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onRemoveDose(day.id, dose.id)}
|
onClick={() => onRemoveDose(day.id, dose.id)}
|
||||||
@@ -139,8 +279,9 @@ const DaySchedule: React.FC<DayScheduleProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
)})}
|
||||||
|
|
||||||
{/* Add day button */}
|
{/* Add day button */}
|
||||||
{canAddDay && (
|
{canAddDay && (
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const Settings = ({
|
|||||||
increment={1}
|
increment={1}
|
||||||
min={3}
|
min={3}
|
||||||
max={7}
|
max={7}
|
||||||
unit={t('days')}
|
unit={t('unitDays')}
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={t('errorNumberRequired')}
|
errorMessage={t('errorNumberRequired')}
|
||||||
/>
|
/>
|
||||||
@@ -128,7 +128,7 @@ const Settings = ({
|
|||||||
increment={1}
|
increment={1}
|
||||||
min={1}
|
min={1}
|
||||||
max={parseInt(simulationDays, 10) || 3}
|
max={parseInt(simulationDays, 10) || 3}
|
||||||
unit={t('days')}
|
unit={t('unitDays')}
|
||||||
required={true}
|
required={true}
|
||||||
errorMessage={t('errorNumberRequired')}
|
errorMessage={t('errorNumberRequired')}
|
||||||
/>
|
/>
|
||||||
@@ -140,7 +140,7 @@ const Settings = ({
|
|||||||
<FormNumericInput
|
<FormNumericInput
|
||||||
value={yAxisMin}
|
value={yAxisMin}
|
||||||
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
||||||
increment={5}
|
increment={1}
|
||||||
min={0}
|
min={0}
|
||||||
placeholder={t('auto')}
|
placeholder={t('auto')}
|
||||||
allowEmpty={true}
|
allowEmpty={true}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const SimulationChart = ({
|
|||||||
displayedDays,
|
displayedDays,
|
||||||
yAxisMin,
|
yAxisMin,
|
||||||
yAxisMax,
|
yAxisMax,
|
||||||
|
days,
|
||||||
t
|
t
|
||||||
}: any) => {
|
}: any) => {
|
||||||
const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
|
const totalHours = (parseInt(simulationDays, 10) || 3) * 24;
|
||||||
@@ -79,6 +80,62 @@ const SimulationChart = ({
|
|||||||
return [domainMin, domainMax];
|
return [domainMin, domainMax];
|
||||||
}, [yAxisMin, yAxisMax]);
|
}, [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
|
// Merge all profiles into a single dataset for proper tooltip synchronization
|
||||||
const mergedData = React.useMemo(() => {
|
const mergedData = React.useMemo(() => {
|
||||||
const dataMap = new Map();
|
const dataMap = new Map();
|
||||||
@@ -93,17 +150,23 @@ const SimulationChart = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add template profile data (regular plan only) if provided
|
// Add template profile data (regular plan only) if provided
|
||||||
|
// Only include points for days that have deviations
|
||||||
templateProfile?.forEach((point: any) => {
|
templateProfile?.forEach((point: any) => {
|
||||||
|
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 };
|
const existing = dataMap.get(point.timeHours) || { timeHours: point.timeHours };
|
||||||
dataMap.set(point.timeHours, {
|
dataMap.set(point.timeHours, {
|
||||||
...existing,
|
...existing,
|
||||||
templateDamph: point.damph,
|
templateDamph: point.damph,
|
||||||
templateLdx: point.ldx
|
templateLdx: point.ldx
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(dataMap.values()).sort((a, b) => a.timeHours - b.timeHours);
|
return Array.from(dataMap.values()).sort((a, b) => a.timeHours - b.timeHours);
|
||||||
}, [combinedProfile, templateProfile]);
|
}, [combinedProfile, templateProfile, daysWithDeviations]);
|
||||||
|
|
||||||
// Calculate chart dimensions
|
// Calculate chart dimensions
|
||||||
const [containerWidth, setContainerWidth] = React.useState(1000);
|
const [containerWidth, setContainerWidth] = React.useState(1000);
|
||||||
@@ -143,6 +206,11 @@ const SimulationChart = ({
|
|||||||
align="left"
|
align="left"
|
||||||
height={36}
|
height={36}
|
||||||
wrapperStyle={{ paddingLeft: 0 }}
|
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 */}
|
{/* Invisible lines just to show in legend */}
|
||||||
{(chartView === 'damph' || chartView === 'both') && (
|
{(chartView === 'damph' || chartView === 'both') && (
|
||||||
@@ -166,7 +234,7 @@ const SimulationChart = ({
|
|||||||
strokeOpacity={0}
|
strokeOpacity={0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{templateProfile && (chartView === 'damph' || chartView === 'both') && (
|
{templateProfile && daysWithDeviations.size > 0 && (chartView === 'damph' || chartView === 'both') && (
|
||||||
<Line
|
<Line
|
||||||
dataKey="templateDamph"
|
dataKey="templateDamph"
|
||||||
name={`${t('dAmphetamine')} (${t('regularPlanOverlay')})`}
|
name={`${t('dAmphetamine')} (${t('regularPlanOverlay')})`}
|
||||||
@@ -175,9 +243,10 @@ const SimulationChart = ({
|
|||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
dot={false}
|
dot={false}
|
||||||
strokeOpacity={0}
|
strokeOpacity={0}
|
||||||
|
opacity={0.5}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{templateProfile && (chartView === 'ldx' || chartView === 'both') && (
|
{templateProfile && daysWithDeviations.size > 0 && (chartView === 'ldx' || chartView === 'both') && (
|
||||||
<Line
|
<Line
|
||||||
dataKey="templateLdx"
|
dataKey="templateLdx"
|
||||||
name={`${t('lisdexamfetamine')} (${t('regularPlanOverlay')})`}
|
name={`${t('lisdexamfetamine')} (${t('regularPlanOverlay')})`}
|
||||||
@@ -186,6 +255,7 @@ const SimulationChart = ({
|
|||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
dot={false}
|
dot={false}
|
||||||
strokeOpacity={0}
|
strokeOpacity={0}
|
||||||
|
opacity={0.5}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</LineChart>
|
</LineChart>
|
||||||
@@ -235,7 +305,7 @@ const SimulationChart = ({
|
|||||||
};
|
};
|
||||||
return <XAxis
|
return <XAxis
|
||||||
xAxisId="hours"
|
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"
|
dataKey="timeHours"
|
||||||
type="number"
|
type="number"
|
||||||
domain={[0, totalHours]}
|
domain={[0, totalHours]}
|
||||||
@@ -248,17 +318,57 @@ const SimulationChart = ({
|
|||||||
|
|
||||||
<YAxis
|
<YAxis
|
||||||
yAxisId="concentration"
|
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}
|
domain={chartDomain as any}
|
||||||
allowDecimals={false}
|
allowDecimals={false}
|
||||||
tickCount={20}
|
tickCount={20}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={(value: any, name) => [`${typeof value === 'number' ? value.toFixed(1) : value} ${t('ngml')}`, name]}
|
content={({ active, payload, label }) => {
|
||||||
labelFormatter={(label, payload) => {
|
if (!active || !payload || payload.length === 0) return null;
|
||||||
|
|
||||||
// Extract timeHours from the payload data point
|
// Extract timeHours from the payload data point
|
||||||
const timeHours = payload?.[0]?.payload?.timeHours ?? label;
|
const timeHours = payload[0]?.payload?.timeHours ?? label;
|
||||||
return `${t('hour').replace('h', 'Hour')}: ${timeHours}${t('hour')}`;
|
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 }}
|
wrapperStyle={{ pointerEvents: 'none', zIndex: 200 }}
|
||||||
allowEscapeViewBox={{ x: false, y: false }}
|
allowEscapeViewBox={{ x: false, y: false }}
|
||||||
@@ -267,12 +377,12 @@ const SimulationChart = ({
|
|||||||
/>
|
/>
|
||||||
<CartesianGrid strokeDasharray="1 1" xAxisId="hours" yAxisId="concentration" />
|
<CartesianGrid strokeDasharray="1 1" xAxisId="hours" yAxisId="concentration" />
|
||||||
|
|
||||||
{showDayReferenceLines !== false && [...Array(dispDays).keys()].map(day => (
|
{showDayReferenceLines !== false && [...Array(dispDays+1).keys()].map(day => (
|
||||||
<ReferenceLine
|
<ReferenceLine
|
||||||
key={`day-${day+1}`}
|
key={`day-${day+1}`}
|
||||||
x={24 * (day+1)}
|
x={24 * (day+1)}
|
||||||
label={{
|
label={{
|
||||||
value: (day === 0 ? t('refLineRegularPlan') : t('refLineDeviatingPlan')) + ' (' + t('refLineDayX', { x: day+1 }) + ')',
|
value: t('refLineDayX', { x: day+1 }) + '→' + getDayLabel(day + 1),
|
||||||
position: 'insideTopRight',
|
position: 'insideTopRight',
|
||||||
style: {
|
style: {
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
@@ -351,7 +461,7 @@ const SimulationChart = ({
|
|||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="templateDamph"
|
dataKey="templateDamph"
|
||||||
name={`${t('dAmphetamine')} (${t('regularPlan')} ${t('continuation')})`}
|
name={`${t('dAmphetamine')} (${t('regularPlanOverlay')})`}
|
||||||
stroke={CHART_COLORS.idealDamph}
|
stroke={CHART_COLORS.idealDamph}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
@@ -366,7 +476,7 @@ const SimulationChart = ({
|
|||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="templateLdx"
|
dataKey="templateLdx"
|
||||||
name={`${t('lisdexamfetamine')} (${t('regularPlan')} ${t('continuation')})`}
|
name={`${t('lisdexamfetamine')} (${t('regularPlanOverlay')})`}
|
||||||
stroke={CHART_COLORS.idealLdx}
|
stroke={CHART_COLORS.idealLdx}
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Minus, Plus, X } from "lucide-react"
|
|||||||
import { Button } from "./button"
|
import { Button } from "./button"
|
||||||
import { Input } from "./input"
|
import { Input } from "./input"
|
||||||
import { cn } from "../../lib/utils"
|
import { cn } from "../../lib/utils"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
interface NumericInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
interface NumericInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
||||||
value: string | number
|
value: string | number
|
||||||
@@ -50,9 +51,11 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [showError, setShowError] = React.useState(false)
|
const { t } = useTranslation()
|
||||||
const [showWarning, setShowWarning] = React.useState(false)
|
const [, setShowError] = React.useState(false)
|
||||||
|
const [, setShowWarning] = React.useState(false)
|
||||||
const [touched, setTouched] = React.useState(false)
|
const [touched, setTouched] = React.useState(false)
|
||||||
|
const [isFocused, setIsFocused] = React.useState(false)
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null)
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// Check if value is invalid (check validity regardless of touch state)
|
// 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 handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
const val = e.target.value
|
const inputValue = e.target.value.trim()
|
||||||
setTouched(true)
|
setTouched(true)
|
||||||
|
setIsFocused(false)
|
||||||
setShowError(false)
|
setShowError(false)
|
||||||
|
setShowWarning(false)
|
||||||
|
|
||||||
if (val === '' && !allowEmpty) {
|
if (inputValue === '' && !allowEmpty) {
|
||||||
|
// Update parent with empty value so validation works
|
||||||
|
onChange('')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (val !== '' && !isNaN(Number(val))) {
|
if (inputValue !== '' && !isNaN(Number(inputValue))) {
|
||||||
onChange(formatValue(val))
|
onChange(formatValue(inputValue))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
|
setIsFocused(true)
|
||||||
setShowError(hasError)
|
setShowError(hasError)
|
||||||
setShowWarning(hasWarning)
|
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 = () => {
|
const getAlignmentClass = () => {
|
||||||
switch (align) {
|
switch (align) {
|
||||||
case 'left': return 'text-left'
|
case 'left': return 'text-left'
|
||||||
@@ -150,7 +170,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
|||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-9 rounded-r-none border-r-0",
|
"h-9 w-9 rounded-r-none border-r-0",
|
||||||
hasError && "border-destructive"
|
hasError && "border-destructive",
|
||||||
|
hasWarning && !hasError && "border-yellow-500"
|
||||||
)}
|
)}
|
||||||
onClick={() => updateValue(-1)}
|
onClick={() => updateValue(-1)}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
@@ -169,7 +190,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
|||||||
"w-20 h-9 z-20",
|
"w-20 h-9 z-20",
|
||||||
"rounded-none",
|
"rounded-none",
|
||||||
getAlignmentClass(),
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -180,7 +202,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
|||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-9",
|
"h-9 w-9",
|
||||||
clearButton && allowEmpty ? "rounded-l-none rounded-r-none border-x-0" : "rounded-l-none border-l-0",
|
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)}
|
onClick={() => updateValue(1)}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
@@ -194,23 +217,25 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
|||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-9 rounded-l-none",
|
"h-9 w-9 rounded-l-none",
|
||||||
hasError && "border-destructive"
|
hasError && "border-destructive",
|
||||||
|
hasWarning && !hasError && "border-yellow-500"
|
||||||
)}
|
)}
|
||||||
onClick={() => onChange('')}
|
onClick={() => onChange('')}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
title={ t('buttonClear') }
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
{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">
|
<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}
|
{errorMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasWarning && showWarning && warningMessage && (
|
{hasWarning && isFocused && 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">
|
<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}
|
{warningMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Button } from "./button"
|
|||||||
import { Input } from "./input"
|
import { Input } from "./input"
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "./popover"
|
import { Popover, PopoverContent, PopoverTrigger } from "./popover"
|
||||||
import { cn } from "../../lib/utils"
|
import { cn } from "../../lib/utils"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
interface TimeInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
interface TimeInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
||||||
value: string
|
value: string
|
||||||
@@ -41,13 +42,22 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [displayValue, setDisplayValue] = React.useState(value)
|
const [displayValue, setDisplayValue] = React.useState(value)
|
||||||
const [isPickerOpen, setIsPickerOpen] = React.useState(false)
|
const [isPickerOpen, setIsPickerOpen] = React.useState(false)
|
||||||
const [showError, setShowError] = React.useState(false)
|
const [, setShowError] = React.useState(false)
|
||||||
const [showWarning, setShowWarning] = 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)
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Current committed value parsed from prop
|
||||||
const [pickerHours, pickerMinutes] = (value || "00:00").split(':').map(Number)
|
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)
|
// Check if value is invalid (check validity regardless of touch state)
|
||||||
const isInvalid = required && (!value || value.trim() === '')
|
const isInvalid = required && (!value || value.trim() === '')
|
||||||
const hasError = error || isInvalid
|
const hasError = error || isInvalid
|
||||||
@@ -57,9 +67,21 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
|||||||
setDisplayValue(value)
|
setDisplayValue(value)
|
||||||
}, [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 handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
const inputValue = e.target.value.trim()
|
const inputValue = e.target.value.trim()
|
||||||
|
setTouched(true)
|
||||||
|
setIsFocused(false)
|
||||||
setShowError(false)
|
setShowError(false)
|
||||||
|
setShowWarning(false)
|
||||||
|
|
||||||
if (inputValue === '') {
|
if (inputValue === '') {
|
||||||
// Update parent with empty value so validation works
|
// Update parent with empty value so validation works
|
||||||
@@ -98,21 +120,41 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
|
setIsFocused(true)
|
||||||
setShowError(hasError)
|
setShowError(hasError)
|
||||||
setShowWarning(hasWarning)
|
setShowWarning(hasWarning)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePickerChange = (part: 'h' | 'm', val: number) => {
|
const handlePickerOpen = (open: boolean) => {
|
||||||
let newHours = pickerHours, newMinutes = pickerMinutes
|
setIsPickerOpen(open)
|
||||||
if (part === 'h') {
|
if (open) {
|
||||||
newHours = val
|
// Reset staging when opening picker
|
||||||
} else {
|
setStagedHour(null)
|
||||||
newMinutes = val
|
setStagedMinute(null)
|
||||||
}
|
}
|
||||||
const formattedTime = `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}`
|
}
|
||||||
|
|
||||||
|
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)
|
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 = () => {
|
const getAlignmentClass = () => {
|
||||||
switch (align) {
|
switch (align) {
|
||||||
case 'left': return 'text-left'
|
case 'left': return 'text-left'
|
||||||
@@ -142,7 +184,7 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Popover open={isPickerOpen} onOpenChange={setIsPickerOpen}>
|
<Popover open={isPickerOpen} onOpenChange={handlePickerOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -158,59 +200,73 @@ const FormTimeInput = React.forwardRef<HTMLInputElement, TimeInputProps>(
|
|||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-3 bg-popover shadow-md border">
|
<PopoverContent className="w-auto p-3 bg-popover shadow-md border">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="text-xs font-medium text-center mb-1">Hour</div>
|
<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">
|
<div className="grid grid-cols-4 gap-1 max-h-60 overflow-y-auto">
|
||||||
{Array.from({ length: 24 }, (_, i) => (
|
{Array.from({ length: 24 }, (_, i) => {
|
||||||
|
const isCurrentValue = pickerHours === i && stagedHour === null
|
||||||
|
const isStaged = stagedHour === i
|
||||||
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={i}
|
key={i}
|
||||||
type="button"
|
type="button"
|
||||||
variant={pickerHours === i ? "default" : "outline"}
|
variant={isStaged ? "default" : isCurrentValue ? "secondary" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-10"
|
className="h-8 w-10"
|
||||||
onClick={() => {
|
onClick={() => handleHourClick(i)}
|
||||||
handlePickerChange('h', i)
|
|
||||||
setIsPickerOpen(false)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{String(i).padStart(2, '0')}
|
{String(i).padStart(2, '0')}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="text-xs font-medium text-center mb-1">Min</div>
|
<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">
|
<div className="grid grid-cols-4 gap-1 max-h-60 overflow-y-auto">
|
||||||
{Array.from({ length: 12 }, (_, i) => i * 5).map(minute => (
|
{Array.from({ length: 12 }, (_, i) => i * 5).map(minute => {
|
||||||
|
const isCurrentValue = pickerMinutes === minute && stagedMinute === null
|
||||||
|
const isStaged = stagedMinute === minute
|
||||||
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={minute}
|
key={minute}
|
||||||
type="button"
|
type="button"
|
||||||
variant={pickerMinutes === minute ? "default" : "outline"}
|
variant={isStaged ? "default" : isCurrentValue ? "secondary" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8 w-10"
|
className="h-8 w-10"
|
||||||
onClick={() => {
|
onClick={() => handleMinuteClick(minute)}
|
||||||
handlePickerChange('m', minute)
|
|
||||||
setIsPickerOpen(false)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{String(minute).padStart(2, '0')}
|
{String(minute).padStart(2, '0')}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleApply}
|
||||||
|
disabled={!canApply}
|
||||||
|
>
|
||||||
|
{t('timePickerApply')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
{unit && <span className="text-sm text-muted-foreground whitespace-nowrap">{unit}</span>}
|
{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">
|
<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}
|
{errorMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasWarning && showWarning && warningMessage && (
|
{hasWarning && isFocused && 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">
|
<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}
|
{warningMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -99,12 +99,12 @@ export const getDefaultState = (): AppState => ({
|
|||||||
therapeuticRange: { min: '10.5', max: '11.5' },
|
therapeuticRange: { min: '10.5', max: '11.5' },
|
||||||
doseIncrement: '2.5',
|
doseIncrement: '2.5',
|
||||||
uiSettings: {
|
uiSettings: {
|
||||||
showDayTimeOnXAxis: 'continuous',
|
showDayTimeOnXAxis: '24h',
|
||||||
showTemplateDay: false,
|
showTemplateDay: true,
|
||||||
chartView: 'both',
|
chartView: 'damph',
|
||||||
yAxisMin: '0',
|
yAxisMin: '8',
|
||||||
yAxisMax: '16',
|
yAxisMax: '13',
|
||||||
simulationDays: '3',
|
simulationDays: '5',
|
||||||
displayedDays: '2',
|
displayedDays: '5',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,23 +186,34 @@ export const useAppState = () => {
|
|||||||
days: prev.days.map(day => {
|
days: prev.days.map(day => {
|
||||||
if (day.id !== dayId) return day;
|
if (day.id !== dayId) return day;
|
||||||
|
|
||||||
// Update the dose field
|
// Update the dose field (no auto-sort)
|
||||||
const updatedDoses = day.doses.map(dose =>
|
const updatedDoses = day.doses.map(dose =>
|
||||||
dose.id === doseId ? { ...dose, [field]: value } : dose
|
dose.id === doseId ? { ...dose, [field]: value } : dose
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort by time if time field was changed
|
return {
|
||||||
if (field === 'time') {
|
...day,
|
||||||
updatedDoses.sort((a, b) => {
|
doses: updatedDoses
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
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 timeA = a.time || '00:00';
|
||||||
const timeB = b.time || '00:00';
|
const timeB = b.time || '00:00';
|
||||||
return timeA.localeCompare(timeB);
|
return timeA.localeCompare(timeB);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...day,
|
...day,
|
||||||
doses: updatedDoses
|
doses: sortedDoses
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
@@ -227,6 +238,7 @@ export const useAppState = () => {
|
|||||||
addDoseToDay,
|
addDoseToDay,
|
||||||
removeDoseFromDay,
|
removeDoseFromDay,
|
||||||
updateDoseInDay,
|
updateDoseInDay,
|
||||||
|
sortDosesInDay,
|
||||||
handleReset
|
handleReset
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,10 +44,14 @@ export const de = {
|
|||||||
axisLabelTimeOfDay: "Tageszeit (h)",
|
axisLabelTimeOfDay: "Tageszeit (h)",
|
||||||
tickNoon: "Mittag",
|
tickNoon: "Mittag",
|
||||||
refLineRegularPlan: "Regulärer Plan",
|
refLineRegularPlan: "Regulärer Plan",
|
||||||
refLineDeviatingPlan: "Abweichung",
|
refLineDeviatingPlan: "Abweichung vom Plan",
|
||||||
|
refLineNoDeviation: "Keine Abweichung",
|
||||||
|
refLineRecovering: "Erholung",
|
||||||
|
refLineIrregularIntake: "Irreguläre Einnahme",
|
||||||
refLineDayX: "Tag {{x}}",
|
refLineDayX: "Tag {{x}}",
|
||||||
refLineMin: "Min",
|
refLineMin: "Min",
|
||||||
refLineMax: "Max",
|
refLineMax: "Max",
|
||||||
|
tooltipHour: "Stunde",
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
diagramSettings: "Diagramm-Einstellungen",
|
diagramSettings: "Diagramm-Einstellungen",
|
||||||
@@ -58,10 +62,9 @@ export const de = {
|
|||||||
xAxisFormat24hDesc: "Wiederholender 0-24h Zyklus",
|
xAxisFormat24hDesc: "Wiederholender 0-24h Zyklus",
|
||||||
xAxisFormat12h: "Tageszeit (12h AM/PM)",
|
xAxisFormat12h: "Tageszeit (12h AM/PM)",
|
||||||
xAxisFormat12hDesc: "Wiederholend 12h Zyklus im AM/PM Format",
|
xAxisFormat12hDesc: "Wiederholend 12h Zyklus im AM/PM Format",
|
||||||
showTemplateDayInChart: "Regulären Plan kontinuierlich im Diagramm anzeigen",
|
showTemplateDayInChart: "Regulären Plan einblenden (nur bei abweichenden Tagen)",
|
||||||
showDayReferenceLines: "Tagestrenner anzeigen",
|
showDayReferenceLines: "Tagestrenner anzeigen (Referenzlinien und Status)",
|
||||||
simulationDuration: "Simulationsdauer",
|
simulationDuration: "Simulationsdauer",
|
||||||
days: "Tage",
|
|
||||||
displayedDays: "Sichtbare Tage (im Fokus)",
|
displayedDays: "Sichtbare Tage (im Fokus)",
|
||||||
yAxisRange: "Y-Achsen-Bereich (Zoom)",
|
yAxisRange: "Y-Achsen-Bereich (Zoom)",
|
||||||
yAxisRangeAutoButton: "A",
|
yAxisRangeAutoButton: "A",
|
||||||
@@ -70,7 +73,6 @@ export const de = {
|
|||||||
therapeuticRange: "Therapeutischer Bereich (Referenzlinien)",
|
therapeuticRange: "Therapeutischer Bereich (Referenzlinien)",
|
||||||
dAmphetamineParameters: "d-Amphetamin Parameter",
|
dAmphetamineParameters: "d-Amphetamin Parameter",
|
||||||
halfLife: "Halbwertszeit",
|
halfLife: "Halbwertszeit",
|
||||||
hours: "h",
|
|
||||||
lisdexamfetamineParameters: "Lisdexamfetamin Parameter",
|
lisdexamfetamineParameters: "Lisdexamfetamin Parameter",
|
||||||
conversionHalfLife: "Umwandlungs-Halbwertszeit",
|
conversionHalfLife: "Umwandlungs-Halbwertszeit",
|
||||||
absorptionRate: "Absorptionsrate",
|
absorptionRate: "Absorptionsrate",
|
||||||
@@ -78,8 +80,10 @@ export const de = {
|
|||||||
resetAllSettings: "Alle Einstellungen zurücksetzen",
|
resetAllSettings: "Alle Einstellungen zurücksetzen",
|
||||||
|
|
||||||
// Units
|
// Units
|
||||||
mg: "mg",
|
unitMg: "mg",
|
||||||
ngml: "ng/ml",
|
unitNgml: "ng/ml",
|
||||||
|
unitHour: "h",
|
||||||
|
unitDays: "Tage",
|
||||||
|
|
||||||
// Reset confirmation
|
// 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.",
|
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",
|
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.",
|
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
|
// Field validation
|
||||||
errorNumberRequired: "Bitte gib eine gültige Zahl ein.",
|
errorNumberRequired: "Bitte gib eine gültige Zahl ein.",
|
||||||
errorTimeRequired: "Bitte gib eine gültige Zeitangabe ein.",
|
errorTimeRequired: "Bitte gib eine gültige Zeitangabe ein.",
|
||||||
warningDuplicateTime: "Mehrere Dosen zur gleichen Zeit.",
|
warningDuplicateTime: "Mehrere Dosen zur gleichen Zeit.",
|
||||||
|
warningZeroDose: "Nulldosis hat keine Auswirkung auf die Simulation.",
|
||||||
|
|
||||||
// Day-based schedule
|
// Day-based schedule
|
||||||
regularPlan: "Regulärer Plan",
|
regularPlan: "Regulärer Plan",
|
||||||
deviatingPlan: "Abweichung vom Plan",
|
deviatingPlan: "Abweichung vom Plan",
|
||||||
|
alternativePlan: "Alternativer Plan",
|
||||||
regularPlanOverlay: "Regulär",
|
regularPlanOverlay: "Regulär",
|
||||||
dayNumber: "Tag {{number}}",
|
dayNumber: "Tag {{number}}",
|
||||||
cloneDay: "Tag klonen",
|
cloneDay: "Tag klonen",
|
||||||
@@ -103,6 +112,11 @@ export const de = {
|
|||||||
addDose: "Dosis hinzufügen",
|
addDose: "Dosis hinzufügen",
|
||||||
removeDose: "Dosis entfernen",
|
removeDose: "Dosis entfernen",
|
||||||
removeDay: "Tag entfernen",
|
removeDay: "Tag entfernen",
|
||||||
|
collapseDay: "Tag einklappen",
|
||||||
|
expandDay: "Tag ausklappen",
|
||||||
|
dose: "Dosis",
|
||||||
|
doses: "Dosen",
|
||||||
|
comparedToRegularPlan: "verglichen mit regulärem Plan",
|
||||||
time: "Zeit",
|
time: "Zeit",
|
||||||
ldx: "LDX",
|
ldx: "LDX",
|
||||||
damph: "d-amph",
|
damph: "d-amph",
|
||||||
@@ -112,7 +126,17 @@ export const de = {
|
|||||||
viewingSharedPlan: "Du siehst einen geteilten Plan",
|
viewingSharedPlan: "Du siehst einen geteilten Plan",
|
||||||
saveAsMyPlan: "Als meinen Plan speichern",
|
saveAsMyPlan: "Als meinen Plan speichern",
|
||||||
discardSharedPlan: "Verwerfen",
|
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;
|
export default de;
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ export const en = {
|
|||||||
axisLabelTimeOfDay: "Time of Day (h)",
|
axisLabelTimeOfDay: "Time of Day (h)",
|
||||||
tickNoon: "Noon",
|
tickNoon: "Noon",
|
||||||
refLineRegularPlan: "Regular Plan",
|
refLineRegularPlan: "Regular Plan",
|
||||||
refLineDeviatingPlan: "Deviation",
|
refLineDeviatingPlan: "Deviation from Plan",
|
||||||
|
refLineNoDeviation: "No Deviation",
|
||||||
|
refLineRecovering: "Recovering",
|
||||||
|
refLineIrregularIntake: "Irregular Intake",
|
||||||
refLineDayX: "Day {{x}}",
|
refLineDayX: "Day {{x}}",
|
||||||
refLineMin: "Min",
|
refLineMin: "Min",
|
||||||
refLineMax: "Max",
|
refLineMax: "Max",
|
||||||
@@ -58,10 +61,9 @@ export const en = {
|
|||||||
xAxisFormat24hDesc: "Repeating 0-24h cycle",
|
xAxisFormat24hDesc: "Repeating 0-24h cycle",
|
||||||
xAxisFormat12h: "Time of Day (12h AM/PM)",
|
xAxisFormat12h: "Time of Day (12h AM/PM)",
|
||||||
xAxisFormat12hDesc: "Repeating 12h cycle in AM/PM format",
|
xAxisFormat12hDesc: "Repeating 12h cycle in AM/PM format",
|
||||||
showTemplateDayInChart: "Overlay regular plan in chart",
|
showTemplateDayInChart: "Show Regular Plan (Only for Deviating Days)",
|
||||||
showDayReferenceLines: "Show day separators",
|
showDayReferenceLines: "Show Day Separators (Reference Lines and Status)",
|
||||||
simulationDuration: "Simulation Duration",
|
simulationDuration: "Simulation Duration",
|
||||||
days: "Days",
|
|
||||||
displayedDays: "Visible Days (in Focus)",
|
displayedDays: "Visible Days (in Focus)",
|
||||||
yAxisRange: "Y-Axis Range (Zoom)",
|
yAxisRange: "Y-Axis Range (Zoom)",
|
||||||
yAxisRangeAutoButton: "A",
|
yAxisRangeAutoButton: "A",
|
||||||
@@ -70,7 +72,6 @@ export const en = {
|
|||||||
therapeuticRange: "Therapeutic Range (Reference Lines)",
|
therapeuticRange: "Therapeutic Range (Reference Lines)",
|
||||||
dAmphetamineParameters: "d-Amphetamine Parameters",
|
dAmphetamineParameters: "d-Amphetamine Parameters",
|
||||||
halfLife: "Half-life",
|
halfLife: "Half-life",
|
||||||
hours: "h",
|
|
||||||
lisdexamfetamineParameters: "Lisdexamfetamine Parameters",
|
lisdexamfetamineParameters: "Lisdexamfetamine Parameters",
|
||||||
conversionHalfLife: "Conversion Half-life",
|
conversionHalfLife: "Conversion Half-life",
|
||||||
absorptionRate: "Absorption Rate",
|
absorptionRate: "Absorption Rate",
|
||||||
@@ -78,8 +79,10 @@ export const en = {
|
|||||||
resetAllSettings: "Reset All Settings",
|
resetAllSettings: "Reset All Settings",
|
||||||
|
|
||||||
// Units
|
// Units
|
||||||
mg: "mg",
|
unitMg: "mg",
|
||||||
ngml: "ng/ml",
|
unitNgml: "ng/ml",
|
||||||
|
unitHour: "h",
|
||||||
|
unitDays: "Days",
|
||||||
|
|
||||||
// Reset confirmation
|
// Reset confirmation
|
||||||
resetConfirmation: "Are you sure you want to reset all settings to default values? This cannot be undone.",
|
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",
|
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.",
|
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
|
// Field validation
|
||||||
errorNumberRequired: "Please enter a valid number.",
|
errorNumberRequired: "Please enter a valid number.",
|
||||||
errorTimeRequired: "Please enter a valid time.",
|
errorTimeRequired: "Please enter a valid time.",
|
||||||
warningDuplicateTime: "Multiple doses at same 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
|
// Day-based schedule
|
||||||
regularPlan: "Regular Plan",
|
regularPlan: "Regular Plan",
|
||||||
deviatingPlan: "Deviation from Plan",
|
deviatingPlan: "Deviation from Plan",
|
||||||
|
alternativePlan: "Alternative Plan",
|
||||||
regularPlanOverlay: "Regular",
|
regularPlanOverlay: "Regular",
|
||||||
dayNumber: "Day {{number}}",
|
dayNumber: "Day {{number}}",
|
||||||
cloneDay: "Clone day",
|
cloneDay: "Clone day",
|
||||||
@@ -103,16 +121,21 @@ export const en = {
|
|||||||
addDose: "Add dose",
|
addDose: "Add dose",
|
||||||
removeDose: "Remove dose",
|
removeDose: "Remove dose",
|
||||||
removeDay: "Remove day",
|
removeDay: "Remove day",
|
||||||
|
collapseDay: "Collapse day",
|
||||||
|
expandDay: "Expand day",
|
||||||
|
dose: "dose",
|
||||||
|
doses: "doses",
|
||||||
|
comparedToRegularPlan: "compared to regular plan",
|
||||||
time: "Time",
|
time: "Time",
|
||||||
ldx: "LDX",
|
ldx: "LDX",
|
||||||
damph: "d-amph",
|
damph: "d-amph",
|
||||||
|
|
||||||
// URL sharing
|
// URL sharing
|
||||||
sharePlan: "Share Plan",
|
sharePlan: "Share Plan",
|
||||||
viewingSharedPlan: "You are viewing a shared plan",
|
viewingSharedPlan: "Viewing shared plan",
|
||||||
saveAsMyPlan: "Save as My Plan",
|
saveAsMyPlan: "Save as My Plan",
|
||||||
discardSharedPlan: "Discard",
|
discardSharedPlan: "Discard",
|
||||||
planCopiedToClipboard: "Plan link copied to clipboard!",
|
planCopiedToClipboard: "Plan link copied to clipboard!"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default en;
|
export default en;
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
--card-foreground: 0 0% 10%;
|
--card-foreground: 0 0% 10%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 0 0% 10%;
|
--popover-foreground: 0 0% 10%;
|
||||||
--primary: 0 0% 15%;
|
--primary: 217 91% 60%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--primary-foreground: 0 0% 100%;
|
||||||
--secondary: 0 0% 94%;
|
--secondary: 220 15% 88%;
|
||||||
--secondary-foreground: 0 0% 15%;
|
--secondary-foreground: 0 0% 15%;
|
||||||
--muted: 220 10% 95%;
|
--muted: 220 10% 95%;
|
||||||
--muted-foreground: 0 0% 45%;
|
--muted-foreground: 0 0% 45%;
|
||||||
|
|||||||
Reference in New Issue
Block a user