Compare commits
8 Commits
955d3ad650
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f64372b94 | |||
| 89c26fb20c | |||
| 48b2ead287 | |||
| a41189bff2 | |||
| a5bb698250 | |||
| 2cd001644e | |||
| fbba3d6122 | |||
| a1298d64a7 |
@@ -315,6 +315,7 @@ const MedPlanAssistant = () => {
|
|||||||
onSwitchProfile={switchProfile}
|
onSwitchProfile={switchProfile}
|
||||||
onSaveProfile={saveProfile}
|
onSaveProfile={saveProfile}
|
||||||
onSaveProfileAs={saveProfileAs}
|
onSaveProfileAs={saveProfileAs}
|
||||||
|
onRenameProfile={updateProfileName}
|
||||||
onDeleteProfile={deleteProfile}
|
onDeleteProfile={deleteProfile}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
@@ -333,7 +334,7 @@ const MedPlanAssistant = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Settings */}
|
{/* Right Column - Settings */}
|
||||||
<div className="lg:col-span-1 space-y-6">
|
<div className="lg:col-span-1 space-y-4">
|
||||||
<Settings
|
<Settings
|
||||||
pkParams={pkParams}
|
pkParams={pkParams}
|
||||||
therapeuticRange={therapeuticRange}
|
therapeuticRange={therapeuticRange}
|
||||||
@@ -351,6 +352,7 @@ const MedPlanAssistant = () => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
<footer className="mt-8 p-4 bg-muted rounded-lg text-sm border">
|
<footer className="mt-8 p-4 bg-muted rounded-lg text-sm border">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from './ui/select';
|
} from './ui/select';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||||
import { Save, Trash2, Plus } from 'lucide-react';
|
import { Save, Trash2, Plus, Pencil } from 'lucide-react';
|
||||||
import { IconButtonWithTooltip } from './ui/icon-button-with-tooltip';
|
import { IconButtonWithTooltip } from './ui/icon-button-with-tooltip';
|
||||||
import { MAX_PROFILES, type ScheduleProfile } from '../constants/defaults';
|
import { MAX_PROFILES, type ScheduleProfile } from '../constants/defaults';
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ interface ProfileSelectorProps {
|
|||||||
onSwitchProfile: (profileId: string) => void;
|
onSwitchProfile: (profileId: string) => void;
|
||||||
onSaveProfile: () => void;
|
onSaveProfile: () => void;
|
||||||
onSaveProfileAs: (name: string) => string | null;
|
onSaveProfileAs: (name: string) => string | null;
|
||||||
|
onRenameProfile: (profileId: string, newName: string) => void;
|
||||||
onDeleteProfile: (profileId: string) => boolean;
|
onDeleteProfile: (profileId: string) => boolean;
|
||||||
t: (key: string) => string;
|
t: (key: string) => string;
|
||||||
}
|
}
|
||||||
@@ -43,20 +44,29 @@ export const ProfileSelector: React.FC<ProfileSelectorProps> = ({
|
|||||||
onSwitchProfile,
|
onSwitchProfile,
|
||||||
onSaveProfile,
|
onSaveProfile,
|
||||||
onSaveProfileAs,
|
onSaveProfileAs,
|
||||||
|
onRenameProfile,
|
||||||
onDeleteProfile,
|
onDeleteProfile,
|
||||||
t,
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
const [newProfileName, setNewProfileName] = useState('');
|
const [newProfileName, setNewProfileName] = useState('');
|
||||||
const [isSaveAsMode, setIsSaveAsMode] = useState(false);
|
const [isSaveAsMode, setIsSaveAsMode] = useState(false);
|
||||||
|
const [isRenameMode, setIsRenameMode] = useState(false);
|
||||||
|
const [renameName, setRenameName] = useState('');
|
||||||
|
|
||||||
const activeProfile = profiles.find(p => p.id === activeProfileId);
|
const activeProfile = profiles.find(p => p.id === activeProfileId);
|
||||||
const canDelete = profiles.length > 1;
|
const canDelete = profiles.length > 1;
|
||||||
const canCreateNew = profiles.length < MAX_PROFILES;
|
const canCreateNew = profiles.length < MAX_PROFILES;
|
||||||
|
|
||||||
|
// Sort profiles alphabetically (case-insensitive)
|
||||||
|
const sortedProfiles = [...profiles].sort((a, b) =>
|
||||||
|
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectChange = (value: string) => {
|
const handleSelectChange = (value: string) => {
|
||||||
if (value === '__new__') {
|
if (value === '__new__') {
|
||||||
// Enter "save as" mode
|
// Enter "save as" mode
|
||||||
setIsSaveAsMode(true);
|
setIsSaveAsMode(true);
|
||||||
|
setIsRenameMode(false);
|
||||||
setNewProfileName('');
|
setNewProfileName('');
|
||||||
} else {
|
} else {
|
||||||
// Confirm before switching if there are unsaved changes
|
// Confirm before switching if there are unsaved changes
|
||||||
@@ -67,6 +77,7 @@ export const ProfileSelector: React.FC<ProfileSelectorProps> = ({
|
|||||||
}
|
}
|
||||||
onSwitchProfile(value);
|
onSwitchProfile(value);
|
||||||
setIsSaveAsMode(false);
|
setIsSaveAsMode(false);
|
||||||
|
setIsRenameMode(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,6 +125,51 @@ export const ProfileSelector: React.FC<ProfileSelectorProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStartRename = () => {
|
||||||
|
if (activeProfile) {
|
||||||
|
setIsRenameMode(true);
|
||||||
|
setIsSaveAsMode(false);
|
||||||
|
setRenameName(activeProfile.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = () => {
|
||||||
|
if (!renameName.trim() || !activeProfile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedName = renameName.trim();
|
||||||
|
|
||||||
|
// Check if name is unchanged
|
||||||
|
if (trimmedName === activeProfile.name) {
|
||||||
|
setIsRenameMode(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate names (excluding current profile)
|
||||||
|
const isDuplicate = profiles.some(
|
||||||
|
p => p.id !== activeProfile.id && p.name.toLowerCase() === trimmedName.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
alert(t('profileNameAlreadyExists') || 'A schedule with this name already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onRenameProfile(activeProfile.id, trimmedName);
|
||||||
|
setIsRenameMode(false);
|
||||||
|
setRenameName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRenameKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleRename();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setIsRenameMode(false);
|
||||||
|
setRenameName('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="mb-4">
|
<Card className="mb-4">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
@@ -135,21 +191,32 @@ export const ProfileSelector: React.FC<ProfileSelectorProps> = ({
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={t('profileSaveAsPlaceholder')}
|
placeholder={t('profileSaveAsPlaceholder')}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="h-9 rounded-r-none border-r-0 w-[360px] bg-background"
|
className="h-9 rounded-r-none border-r-0 w-[288px] bg-background"
|
||||||
|
/>
|
||||||
|
) : isRenameMode ? (
|
||||||
|
<Input
|
||||||
|
id="profile-selector"
|
||||||
|
type="text"
|
||||||
|
value={renameName}
|
||||||
|
onChange={(e) => setRenameName(e.target.value)}
|
||||||
|
onKeyDown={handleRenameKeyDown}
|
||||||
|
placeholder={t('profileRenamePlaceholder')}
|
||||||
|
autoFocus
|
||||||
|
className="h-9 rounded-r-none border-r-0 w-[288px] bg-background"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Select
|
<Select
|
||||||
value={activeProfileId}
|
value={activeProfileId}
|
||||||
onValueChange={handleSelectChange}
|
onValueChange={handleSelectChange}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="profile-selector" className="h-9 rounded-r-none border-r-0 w-[360px] bg-background">
|
<SelectTrigger id="profile-selector" className="h-9 rounded-r-none border-r-0 w-[288px] bg-background">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
{activeProfile?.name}
|
{activeProfile?.name}
|
||||||
{hasUnsavedChanges && ' *'}
|
{hasUnsavedChanges && ' *'}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{profiles.map(profile => (
|
{sortedProfiles.map(profile => (
|
||||||
<SelectItem key={profile.id} value={profile.id}>
|
<SelectItem key={profile.id} value={profile.id}>
|
||||||
{profile.name}
|
{profile.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -178,10 +245,21 @@ export const ProfileSelector: React.FC<ProfileSelectorProps> = ({
|
|||||||
|
|
||||||
{/* Save button - integrated */}
|
{/* Save button - integrated */}
|
||||||
<IconButtonWithTooltip
|
<IconButtonWithTooltip
|
||||||
onClick={isSaveAsMode ? handleSaveAs : onSaveProfile}
|
onClick={isSaveAsMode ? handleSaveAs : isRenameMode ? handleRename : onSaveProfile}
|
||||||
icon={<Save className="h-4 w-4" />}
|
icon={<Save className="h-4 w-4" />}
|
||||||
tooltip={isSaveAsMode ? t('profileSaveAs') : t('profileSave')}
|
tooltip={isSaveAsMode ? t('profileSaveAs') : isRenameMode ? t('profileRename') : t('profileSave')}
|
||||||
disabled={(isSaveAsMode && !newProfileName.trim()) || (!isSaveAsMode && !hasUnsavedChanges)}
|
disabled={(isSaveAsMode && !newProfileName.trim()) || (isRenameMode && !renameName.trim()) || (!isSaveAsMode && !isRenameMode && !hasUnsavedChanges)}
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-none border-r-0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Rename button - integrated */}
|
||||||
|
<IconButtonWithTooltip
|
||||||
|
onClick={handleStartRename}
|
||||||
|
icon={<Pencil className="h-4 w-4" />}
|
||||||
|
tooltip={t('profileRename')}
|
||||||
|
disabled={isSaveAsMode || isRenameMode}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="rounded-none border-r-0"
|
className="rounded-none border-r-0"
|
||||||
@@ -192,7 +270,7 @@ export const ProfileSelector: React.FC<ProfileSelectorProps> = ({
|
|||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
icon={<Trash2 className="h-4 w-4" />}
|
icon={<Trash2 className="h-4 w-4" />}
|
||||||
tooltip={canDelete ? t('profileDelete') : t('profileDeleteDisabled')}
|
tooltip={canDelete ? t('profileDelete') : t('profileDeleteDisabled')}
|
||||||
disabled={!canDelete || isSaveAsMode}
|
disabled={!canDelete || isSaveAsMode || isRenameMode}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="rounded-l-none text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
className="rounded-l-none text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||||
@@ -216,6 +294,24 @@ export const ProfileSelector: React.FC<ProfileSelectorProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Helper text for rename mode */}
|
||||||
|
{isRenameMode && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="text-xs text-muted-foreground flex-1">
|
||||||
|
{t('profileRenameHelp')}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsRenameMode(false);
|
||||||
|
setRenameName('');
|
||||||
|
}}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground underline"
|
||||||
|
>
|
||||||
|
{t('cancel')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -394,11 +394,12 @@ const Settings = ({
|
|||||||
min={0}
|
min={0}
|
||||||
max={500}
|
max={500}
|
||||||
placeholder={t('min')}
|
placeholder={t('min')}
|
||||||
required={true}
|
required={false}
|
||||||
error={!!therapeuticRangeError || !therapeuticRange.min}
|
error={!!therapeuticRangeError}
|
||||||
errorMessage={formatText(therapeuticRangeError || t('errorTherapeuticRangeMinRequired') || 'Minimum therapeutic range is required')}
|
errorMessage={formatText(therapeuticRangeError)}
|
||||||
showResetButton={true}
|
showResetButton={true}
|
||||||
defaultValue={defaultsForT.therapeuticRangeMin}
|
defaultValue={defaultsForT.therapeuticRangeMin}
|
||||||
|
allowEmpty={true}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted-foreground">-</span>
|
<span className="text-muted-foreground">-</span>
|
||||||
<FormNumericInput
|
<FormNumericInput
|
||||||
@@ -409,11 +410,12 @@ const Settings = ({
|
|||||||
max={500}
|
max={500}
|
||||||
placeholder={t('max')}
|
placeholder={t('max')}
|
||||||
unit="ng/ml"
|
unit="ng/ml"
|
||||||
required={true}
|
required={false}
|
||||||
error={!!therapeuticRangeError || !therapeuticRange.max}
|
error={!!therapeuticRangeError}
|
||||||
errorMessage={formatText(therapeuticRangeError || t('errorTherapeuticRangeMaxRequired') || 'Maximum therapeutic range is required')}
|
errorMessage={formatText(therapeuticRangeError)}
|
||||||
showResetButton={true}
|
showResetButton={true}
|
||||||
defaultValue={defaultsForT.therapeuticRangeMax}
|
defaultValue={defaultsForT.therapeuticRangeMax}
|
||||||
|
allowEmpty={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -517,7 +519,7 @@ const Settings = ({
|
|||||||
onValueChange={value => onUpdateUiSetting('showDayTimeOnXAxis', value)}
|
onValueChange={value => onUpdateUiSetting('showDayTimeOnXAxis', value)}
|
||||||
showResetButton={true}
|
showResetButton={true}
|
||||||
defaultValue={defaultsForT.showDayTimeOnXAxis}
|
defaultValue={defaultsForT.showDayTimeOnXAxis}
|
||||||
triggerClassName="w-[360px]"
|
triggerClassName="w-[288px]"
|
||||||
>
|
>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -826,7 +828,7 @@ const Settings = ({
|
|||||||
onValueChange={(value) => updateAdvanced('standardVd', 'preset', value as 'adult' | 'child' | 'custom' | 'weight-based')}
|
onValueChange={(value) => updateAdvanced('standardVd', 'preset', value as 'adult' | 'child' | 'custom' | 'weight-based')}
|
||||||
showResetButton={true}
|
showResetButton={true}
|
||||||
defaultValue={defaultsForT.standardVdPreset}
|
defaultValue={defaultsForT.standardVdPreset}
|
||||||
triggerClassName="w-[360px]"
|
triggerClassName="w-[288px]"
|
||||||
>
|
>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="adult">{t('standardVdPresetAdult')}</SelectItem>
|
<SelectItem value="adult">{t('standardVdPresetAdult')}</SelectItem>
|
||||||
@@ -961,7 +963,7 @@ const Settings = ({
|
|||||||
}
|
}
|
||||||
showResetButton={true}
|
showResetButton={true}
|
||||||
defaultValue={defaultsForT.urinePh}
|
defaultValue={defaultsForT.urinePh}
|
||||||
triggerClassName="w-[360px]"
|
triggerClassName="w-[288px]"
|
||||||
>
|
>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="normal">{t('urinePHModeNormal')}</SelectItem>
|
<SelectItem value="normal">{t('urinePHModeNormal')}</SelectItem>
|
||||||
@@ -1002,7 +1004,7 @@ const Settings = ({
|
|||||||
}}
|
}}
|
||||||
showResetButton={true}
|
showResetButton={true}
|
||||||
defaultValue={defaultsForT.ageGroup}
|
defaultValue={defaultsForT.ageGroup}
|
||||||
triggerClassName="w-[360px]"
|
triggerClassName="w-[288px]"
|
||||||
>
|
>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="adult">{t('ageGroupAdult')}</SelectItem>
|
<SelectItem value="adult">{t('ageGroupAdult')}</SelectItem>
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ const SimulationChart = React.memo(({
|
|||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const { width: containerWidth } = useElementSize(containerRef, 150);
|
const { width: containerWidth } = useElementSize(containerRef, 150);
|
||||||
|
|
||||||
|
// Guard against invalid dimensions during initial render
|
||||||
|
const yAxisWidth = 80;
|
||||||
|
const minContainerWidth = yAxisWidth + 100; // Minimum 100px for chart area
|
||||||
|
const safeContainerWidth = Math.max(containerWidth, minContainerWidth);
|
||||||
|
|
||||||
// Track current theme for chart styling
|
// Track current theme for chart styling
|
||||||
const [isDarkTheme, setIsDarkTheme] = React.useState(false);
|
const [isDarkTheme, setIsDarkTheme] = React.useState(false);
|
||||||
|
|
||||||
@@ -96,9 +101,8 @@ const SimulationChart = React.memo(({
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Y-axis takes ~80px, scrollable area gets the rest
|
// Calculate scrollable width using safe container width
|
||||||
const yAxisWidth = 80;
|
const scrollableWidth = safeContainerWidth - yAxisWidth;
|
||||||
const scrollableWidth = containerWidth - yAxisWidth;
|
|
||||||
|
|
||||||
// Calculate chart width for scrollable area
|
// Calculate chart width for scrollable area
|
||||||
const chartWidth = simDays <= dispDays
|
const chartWidth = simDays <= dispDays
|
||||||
@@ -106,7 +110,7 @@ const SimulationChart = React.memo(({
|
|||||||
: Math.ceil((scrollableWidth / dispDays) * simDays);
|
: Math.ceil((scrollableWidth / dispDays) * simDays);
|
||||||
|
|
||||||
// Use shorter captions on narrow containers to reduce wrapping
|
// Use shorter captions on narrow containers to reduce wrapping
|
||||||
const isCompactLabels = containerWidth < 640; // tweakable threshold for mobile
|
const isCompactLabels = safeContainerWidth < 640; // tweakable threshold for mobile
|
||||||
|
|
||||||
// Precompute series labels with translations
|
// Precompute series labels with translations
|
||||||
const seriesLabels = React.useMemo<Record<string, { full: string; short: string; display: string }>>(() => {
|
const seriesLabels = React.useMemo<Record<string, { full: string; short: string; display: string }>>(() => {
|
||||||
@@ -145,9 +149,9 @@ const SimulationChart = React.memo(({
|
|||||||
|
|
||||||
// Dynamically calculate tick interval based on available pixel width
|
// Dynamically calculate tick interval based on available pixel width
|
||||||
const xTickInterval = React.useMemo(() => {
|
const xTickInterval = React.useMemo(() => {
|
||||||
// Aim for ~46px per label to avoid overlaps on narrow screens
|
// Aim for ~46px per label to avoid overlaps on narrow screens
|
||||||
//const MIN_PX_PER_TICK = 46;
|
//const MIN_PX_PER_TICK = 46;
|
||||||
const MIN_PX_PER_TICK = 46;
|
const MIN_PX_PER_TICK = 56; // increased to 56, partially too tight otherwise
|
||||||
const intervals = [1, 2, 3, 4, 6, 8, 12, 24];
|
const intervals = [1, 2, 3, 4, 6, 8, 12, 24];
|
||||||
|
|
||||||
const pxPerDay = scrollableWidth / Math.max(1, dispDays);
|
const pxPerDay = scrollableWidth / Math.max(1, dispDays);
|
||||||
@@ -270,7 +274,7 @@ const SimulationChart = React.memo(({
|
|||||||
// User set yAxisMax explicitly
|
// User set yAxisMax explicitly
|
||||||
// Add padding to dataMax and use the higher of manual or (dataMax + padding)
|
// Add padding to dataMax and use the higher of manual or (dataMax + padding)
|
||||||
const range = dataMax - dataMin;
|
const range = dataMax - dataMin;
|
||||||
const padding = range * 0.1;
|
const padding = range * 0.05;
|
||||||
const dataMaxWithPadding = dataMax + padding;
|
const dataMaxWithPadding = dataMax + padding;
|
||||||
// Use manual max only if it's higher than dataMax + padding
|
// Use manual max only if it's higher than dataMax + padding
|
||||||
domainMax = Math.max(numMax, dataMaxWithPadding);
|
domainMax = Math.max(numMax, dataMaxWithPadding);
|
||||||
@@ -279,9 +283,9 @@ const SimulationChart = React.memo(({
|
|||||||
domainMax = numMax;
|
domainMax = numMax;
|
||||||
}
|
}
|
||||||
} else if (dataMax !== -Infinity) { // data exists
|
} else if (dataMax !== -Infinity) { // data exists
|
||||||
// Auto mode: add 10% padding above
|
// Auto mode: add 5% padding above
|
||||||
const range = dataMax - dataMin;
|
const range = dataMax - dataMin;
|
||||||
const padding = range * 0.1;
|
const padding = range * 0.05;
|
||||||
domainMax = dataMax + padding;
|
domainMax = dataMax + padding;
|
||||||
} else { // no data
|
} else { // no data
|
||||||
domainMax = 100;
|
domainMax = 100;
|
||||||
@@ -446,6 +450,15 @@ const SimulationChart = React.memo(({
|
|||||||
);
|
);
|
||||||
}, [seriesLabels]);
|
}, [seriesLabels]);
|
||||||
|
|
||||||
|
// Don't render chart if dimensions are invalid (prevents crash during initialization)
|
||||||
|
if (chartWidth <= 0 || scrollableWidth <= 0) {
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="flex-grow w-full flex flex-col overflow-y-hidden items-center justify-center text-muted-foreground">
|
||||||
|
<p>{t('loadingChart', { defaultValue: 'Loading chart...' })}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Render the chart
|
// Render the chart
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="flex-grow w-full flex flex-col overflow-y-hidden">
|
<div ref={containerRef} className="flex-grow w-full flex flex-col overflow-y-hidden">
|
||||||
@@ -531,7 +544,9 @@ const SimulationChart = React.memo(({
|
|||||||
domain={yAxisDomain as any}
|
domain={yAxisDomain as any}
|
||||||
axisLine={{ stroke: isDarkTheme ? '#ccc' : '#666' }}
|
axisLine={{ stroke: isDarkTheme ? '#ccc' : '#666' }}
|
||||||
tick={<YAxisTick />}
|
tick={<YAxisTick />}
|
||||||
allowDecimals={true}
|
tickCount={20}
|
||||||
|
interval={1}
|
||||||
|
allowDecimals={false}
|
||||||
allowDataOverflow={false}
|
allowDataOverflow={false}
|
||||||
/>
|
/>
|
||||||
<RechartsTooltip
|
<RechartsTooltip
|
||||||
@@ -594,7 +609,7 @@ const SimulationChart = React.memo(({
|
|||||||
style={{ stroke: isDarkTheme ? '#666' : '#ccc' }}
|
style={{ stroke: isDarkTheme ? '#666' : '#ccc' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showDayReferenceLines !== false && [...Array(dispDays + 1).keys()].map(day => {
|
{showDayReferenceLines !== false && [...Array(simDays).keys()].map(day => {
|
||||||
// Determine whether to use compact day labels to avoid overlap on narrow screens
|
// Determine whether to use compact day labels to avoid overlap on narrow screens
|
||||||
const pxPerDay = scrollableWidth / Math.max(1, dispDays);
|
const pxPerDay = scrollableWidth / Math.max(1, dispDays);
|
||||||
let label = "";
|
let label = "";
|
||||||
@@ -625,20 +640,20 @@ const SimulationChart = React.memo(({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{showTherapeuticRange && (chartView === 'damph' || chartView === 'both') && (
|
{showTherapeuticRange && (chartView === 'damph' || chartView === 'both') && therapeuticRange.min && !isNaN(parseFloat(therapeuticRange.min)) && (
|
||||||
<ReferenceLine
|
<ReferenceLine
|
||||||
y={parseFloat(therapeuticRange.min) || 0}
|
y={parseFloat(therapeuticRange.min)}
|
||||||
label={{ value: t('refLineMin'), position: 'insideTopLeft' }}
|
label={{ value: t('refLineMin'), position: 'insideBottomLeft', style: { fontSize: '0.75rem', fontStyle: 'italic', fill: CHART_COLORS.therapeuticMin } }}
|
||||||
stroke={CHART_COLORS.therapeuticMin}
|
stroke={CHART_COLORS.therapeuticMin}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
xAxisId="hours"
|
xAxisId="hours"
|
||||||
yAxisId="concentration"
|
yAxisId="concentration"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showTherapeuticRange && (chartView === 'damph' || chartView === 'both') && (
|
{showTherapeuticRange && (chartView === 'damph' || chartView === 'both') && therapeuticRange.max && !isNaN(parseFloat(therapeuticRange.max)) && (
|
||||||
<ReferenceLine
|
<ReferenceLine
|
||||||
y={parseFloat(therapeuticRange.max) || 0}
|
y={parseFloat(therapeuticRange.max)}
|
||||||
label={{ value: t('refLineMax'), position: 'insideTopLeft' }}
|
label={{ value: t('refLineMax'), position: 'insideTopLeft', style: { fontSize: '0.75rem', fontStyle: 'italic', fill: CHART_COLORS.therapeuticMax } }}
|
||||||
stroke={CHART_COLORS.therapeuticMax}
|
stroke={CHART_COLORS.therapeuticMax}
|
||||||
strokeDasharray="3 3"
|
strokeDasharray="3 3"
|
||||||
xAxisId="hours"
|
xAxisId="hours"
|
||||||
|
|||||||
@@ -38,17 +38,25 @@ export function useElementSize<T extends HTMLElement>(
|
|||||||
const element = ref.current;
|
const element = ref.current;
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
// Set initial size
|
// Set initial size (guard against 0 dimensions)
|
||||||
setSize({
|
const initialWidth = element.clientWidth;
|
||||||
width: element.clientWidth,
|
const initialHeight = element.clientHeight;
|
||||||
height: element.clientHeight,
|
|
||||||
});
|
if (initialWidth > 0 && initialHeight > 0) {
|
||||||
|
setSize({
|
||||||
|
width: initialWidth,
|
||||||
|
height: initialHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Use ResizeObserver for efficient element size tracking
|
// Use ResizeObserver for efficient element size tracking
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const { width, height } = entry.contentRect;
|
const { width, height } = entry.contentRect;
|
||||||
setSize({ width, height });
|
// Guard against invalid dimensions
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
setSize({ width, height });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,15 @@ export const de = {
|
|||||||
profileSaveAsNewProfile: "Als neuen Zeitplan speichern",
|
profileSaveAsNewProfile: "Als neuen Zeitplan speichern",
|
||||||
profileSave: "Änderungen im aktuellen Zeitplan speichern",
|
profileSave: "Änderungen im aktuellen Zeitplan speichern",
|
||||||
profileSaveAs: "Neuen Zeitplan mit aktueller Konfiguration erstellen",
|
profileSaveAs: "Neuen Zeitplan mit aktueller Konfiguration erstellen",
|
||||||
|
profileRename: "Diesen Zeitplan umbenennen",
|
||||||
|
profileRenameHelp: "Geben Sie einen neuen Namen für den Zeitplan ein und drücken Sie Enter oder klicken Sie auf Speichern",
|
||||||
|
profileRenamePlaceholder: "Neuer Name für den Zeitplan...",
|
||||||
profileDelete: "Diesen Zeitplan löschen",
|
profileDelete: "Diesen Zeitplan löschen",
|
||||||
profileDeleteDisabled: "Der letzte Zeitplan kann nicht gelöscht werden",
|
profileDeleteDisabled: "Der letzte Zeitplan kann nicht gelöscht werden",
|
||||||
profileDeleteConfirm: "Möchten Sie den Zeitplan '{name}' wirklich löschen?",
|
profileDeleteConfirm: "Möchten Sie den Zeitplan '{name}' wirklich löschen?",
|
||||||
profileSaveAsPlaceholder: "Name für den neuen Zeitplan...",
|
profileSaveAsPlaceholder: "Name für den neuen Zeitplan...",
|
||||||
profileSaveAsHelp: "Geben Sie einen Namen für den neuen Zeitplan ein und drücken Sie Enter oder klicken Sie auf Speichern",
|
profileSaveAsHelp: "Geben Sie einen Namen für den neuen Zeitplan ein und drücken Sie Enter oder klicken Sie auf Speichern",
|
||||||
|
profileNameAlreadyExists: "Ein Zeitplan mit diesem Namen existiert bereits",
|
||||||
profileSwitchUnsavedConfirm: "Sie haben ungespeicherte Änderungen. Beim Wechseln des Zeitplans gehen diese verloren. Fortfahren?",
|
profileSwitchUnsavedConfirm: "Sie haben ungespeicherte Änderungen. Beim Wechseln des Zeitplans gehen diese verloren. Fortfahren?",
|
||||||
profiles: "Zeitpläne",
|
profiles: "Zeitpläne",
|
||||||
cancel: "Abbrechen",
|
cancel: "Abbrechen",
|
||||||
@@ -114,7 +118,7 @@ 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: "Basis-Zeitplan kontinuierlich anzeigen",
|
showTemplateDayInChart: "Basis-Zeitplan zum Vergleich anzeigen",
|
||||||
showTemplateDayTooltip: "Führt die Simulation des Basis-Zeitplans auch dann fort, auch wenn für Tag 2+ abweichende Zeitpläne definiert sind. Die entsprechenden Plasmakonzentrationen werden, nur im Falle einer Abweichung vom Basis-Zeitplan, als zusätzliche gestrichelte Linien dargestellt.\\n\\n__Standard:__ **aktiviert**",
|
showTemplateDayTooltip: "Führt die Simulation des Basis-Zeitplans auch dann fort, auch wenn für Tag 2+ abweichende Zeitpläne definiert sind. Die entsprechenden Plasmakonzentrationen werden, nur im Falle einer Abweichung vom Basis-Zeitplan, als zusätzliche gestrichelte Linien dargestellt.\\n\\n__Standard:__ **aktiviert**",
|
||||||
simulationSettings: "Simulations-Einstellungen",
|
simulationSettings: "Simulations-Einstellungen",
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,15 @@ export const en = {
|
|||||||
profileSaveAsNewProfile: "Save as new schedule",
|
profileSaveAsNewProfile: "Save as new schedule",
|
||||||
profileSave: "Save changes to current schedule",
|
profileSave: "Save changes to current schedule",
|
||||||
profileSaveAs: "Create new schedule with current configuration",
|
profileSaveAs: "Create new schedule with current configuration",
|
||||||
|
profileRename: "Rename this schedule",
|
||||||
|
profileRenameHelp: "Enter a new name for the schedule and press Enter or click Save",
|
||||||
|
profileRenamePlaceholder: "New name for the schedule...",
|
||||||
profileDelete: "Delete this schedule",
|
profileDelete: "Delete this schedule",
|
||||||
profileDeleteDisabled: "Cannot delete the last schedule",
|
profileDeleteDisabled: "Cannot delete the last schedule",
|
||||||
profileDeleteConfirm: "Are you sure you want to delete the schedule '{name}'?",
|
profileDeleteConfirm: "Are you sure you want to delete the schedule '{name}'?",
|
||||||
profileSaveAsPlaceholder: "Name for the new schedule...",
|
profileSaveAsPlaceholder: "Name for the new schedule...",
|
||||||
profileSaveAsHelp: "Enter a name for the new schedule and press Enter or click Save",
|
profileSaveAsHelp: "Enter a name for the new schedule and press Enter or click Save",
|
||||||
|
profileNameAlreadyExists: "A schedule with this name already exists",
|
||||||
profileSwitchUnsavedConfirm: "You have unsaved changes. Switching schedules will discard them. Continue?",
|
profileSwitchUnsavedConfirm: "You have unsaved changes. Switching schedules will discard them. Continue?",
|
||||||
profiles: "schedules",
|
profiles: "schedules",
|
||||||
cancel: "Cancel",
|
cancel: "Cancel",
|
||||||
@@ -113,7 +117,7 @@ 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: "Continuously Show Baseline Schedule",
|
showTemplateDayInChart: "Show Baseline Schedule for Comparison",
|
||||||
showTemplateDayTooltip: "Continue simulating the baseline schedule even when deviations are defined for day 2+. Corresponding plasma concentrations will be shown as additional dashed lines, only if deviating from the baseline schedule.\\n\\n__Default:__ **enabled**",
|
showTemplateDayTooltip: "Continue simulating the baseline schedule even when deviations are defined for day 2+. Corresponding plasma concentrations will be shown as additional dashed lines, only if deviating from the baseline schedule.\\n\\n__Default:__ **enabled**",
|
||||||
simulationSettings: "Simulation Settings",
|
simulationSettings: "Simulation Settings",
|
||||||
showDayReferenceLines: "Show Day Separators",
|
showDayReferenceLines: "Show Day Separators",
|
||||||
|
|||||||
Reference in New Issue
Block a user