Files
med-plan-assistant/src/components/profile-selector.tsx

320 lines
10 KiB
TypeScript

/**
* Profile Selector Component
*
* Allows users to manage medication schedule profiles with create, save,
* save-as, and delete functionality. Provides a combobox-style interface
* for profile selection and management.
*
* @author Andreas Weyer
* @license MIT
*/
import React, { useState } from 'react';
import { Card, CardContent } from './ui/card';
import { Label } from './ui/label';
import { Input } from './ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from './ui/select';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { Save, Trash2, Plus, Pencil } from 'lucide-react';
import { IconButtonWithTooltip } from './ui/icon-button-with-tooltip';
import { MAX_PROFILES, type ScheduleProfile } from '../constants/defaults';
interface ProfileSelectorProps {
profiles: ScheduleProfile[];
activeProfileId: string;
hasUnsavedChanges: boolean;
onSwitchProfile: (profileId: string) => void;
onSaveProfile: () => void;
onSaveProfileAs: (name: string) => string | null;
onRenameProfile: (profileId: string, newName: string) => void;
onDeleteProfile: (profileId: string) => boolean;
t: (key: string) => string;
}
export const ProfileSelector: React.FC<ProfileSelectorProps> = ({
profiles,
activeProfileId,
hasUnsavedChanges,
onSwitchProfile,
onSaveProfile,
onSaveProfileAs,
onRenameProfile,
onDeleteProfile,
t,
}) => {
const [newProfileName, setNewProfileName] = useState('');
const [isSaveAsMode, setIsSaveAsMode] = useState(false);
const [isRenameMode, setIsRenameMode] = useState(false);
const [renameName, setRenameName] = useState('');
const activeProfile = profiles.find(p => p.id === activeProfileId);
const canDelete = profiles.length > 1;
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) => {
if (value === '__new__') {
// Enter "save as" mode
setIsSaveAsMode(true);
setIsRenameMode(false);
setNewProfileName('');
} else {
// Confirm before switching if there are unsaved changes
if (hasUnsavedChanges) {
if (!window.confirm(t('profileSwitchUnsavedConfirm'))) {
return;
}
}
onSwitchProfile(value);
setIsSaveAsMode(false);
setIsRenameMode(false);
}
};
const handleSaveAs = () => {
if (!newProfileName.trim()) {
return;
}
// Check for duplicate names
const isDuplicate = profiles.some(
p => p.name.toLowerCase() === newProfileName.trim().toLowerCase()
);
let finalName = newProfileName.trim();
if (isDuplicate) {
// Find next available suffix
let suffix = 2;
while (profiles.some(p => p.name.toLowerCase() === `${newProfileName.trim()} (${suffix})`.toLowerCase())) {
suffix++;
}
finalName = `${newProfileName.trim()} (${suffix})`;
}
const newProfileId = onSaveProfileAs(finalName);
if (newProfileId) {
setIsSaveAsMode(false);
setNewProfileName('');
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSaveAs();
} else if (e.key === 'Escape') {
setIsSaveAsMode(false);
setNewProfileName('');
}
};
const handleDelete = () => {
if (activeProfile && canDelete) {
if (window.confirm(t('profileDeleteConfirm')?.replace('{name}', activeProfile.name))) {
onDeleteProfile(activeProfile.id);
}
}
};
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 (
<Card className="mb-4">
<CardContent className="pt-6">
<div className="space-y-2">
{/* Title label */}
<Label htmlFor="profile-selector" className="text-sm font-medium">
{t('savedPlans')}
</Label>
{/* Profile selector with integrated buttons */}
<div className="flex items-stretch">
{/* Profile selector / name input */}
{isSaveAsMode ? (
<Input
id="profile-selector"
type="text"
value={newProfileName}
onChange={(e) => setNewProfileName(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t('profileSaveAsPlaceholder')}
autoFocus
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
value={activeProfileId}
onValueChange={handleSelectChange}
>
<SelectTrigger id="profile-selector" className="h-9 rounded-r-none border-r-0 w-[288px] bg-background">
<SelectValue>
{activeProfile?.name}
{hasUnsavedChanges && ' *'}
</SelectValue>
</SelectTrigger>
<SelectContent>
{sortedProfiles.map(profile => (
<SelectItem key={profile.id} value={profile.id}>
{profile.name}
</SelectItem>
))}
{canCreateNew && (
<>
<div className="my-1 h-px bg-border" />
<Tooltip>
<TooltipTrigger asChild>
<SelectItem value="__new__">
<div className="flex items-center gap-2">
<Plus className="h-4 w-4" />
<span>{t('profileSaveAsNewProfile')}</span>
</div>
</SelectItem>
</TooltipTrigger>
<TooltipContent side="right">
<p className="text-xs">{t('profileSaveAs')}</p>
</TooltipContent>
</Tooltip>
</>
)}
</SelectContent>
</Select>
)}
{/* Save button - integrated */}
<IconButtonWithTooltip
onClick={isSaveAsMode ? handleSaveAs : isRenameMode ? handleRename : onSaveProfile}
icon={<Save className="h-4 w-4" />}
tooltip={isSaveAsMode ? t('profileSaveAs') : isRenameMode ? t('profileRename') : t('profileSave')}
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"
size="icon"
className="rounded-none border-r-0"
/>
{/* Delete button - integrated */}
<IconButtonWithTooltip
onClick={handleDelete}
icon={<Trash2 className="h-4 w-4" />}
tooltip={canDelete ? t('profileDelete') : t('profileDeleteDisabled')}
disabled={!canDelete || isSaveAsMode || isRenameMode}
variant="outline"
size="icon"
className="rounded-l-none text-destructive hover:bg-destructive hover:text-destructive-foreground"
/>
</div>
{/* Helper text for save-as mode */}
{isSaveAsMode && (
<div className="flex items-center gap-2">
<p className="text-xs text-muted-foreground flex-1">
{t('profileSaveAsHelp')}
</p>
<button
onClick={() => {
setIsSaveAsMode(false);
setNewProfileName('');
}}
className="text-xs text-muted-foreground hover:text-foreground underline"
>
{t('cancel')}
</button>
</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>
</CardContent>
</Card>
);
};