Add profile management functionality
- Added profile management functions: createProfile, deleteProfile, switchProfile, saveProfile, saveProfileAs, updateProfileName, and hasUnsavedChanges. - Migrated state management to support profile-based format for schedules. - Updated localizations for profile management features in English and German. - Introduced ProfileSelector component for user interface to manage profiles. - Enhanced export/import functionality to handle profiles and schedules.
This commit is contained in:
223
src/components/profile-selector.tsx
Normal file
223
src/components/profile-selector.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 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 } 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;
|
||||
onDeleteProfile: (profileId: string) => boolean;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export const ProfileSelector: React.FC<ProfileSelectorProps> = ({
|
||||
profiles,
|
||||
activeProfileId,
|
||||
hasUnsavedChanges,
|
||||
onSwitchProfile,
|
||||
onSaveProfile,
|
||||
onSaveProfileAs,
|
||||
onDeleteProfile,
|
||||
t,
|
||||
}) => {
|
||||
const [newProfileName, setNewProfileName] = useState('');
|
||||
const [isSaveAsMode, setIsSaveAsMode] = useState(false);
|
||||
|
||||
const activeProfile = profiles.find(p => p.id === activeProfileId);
|
||||
const canDelete = profiles.length > 1;
|
||||
const canCreateNew = profiles.length < MAX_PROFILES;
|
||||
|
||||
const handleSelectChange = (value: string) => {
|
||||
if (value === '__new__') {
|
||||
// Enter "save as" mode
|
||||
setIsSaveAsMode(true);
|
||||
setNewProfileName('');
|
||||
} else {
|
||||
// Confirm before switching if there are unsaved changes
|
||||
if (hasUnsavedChanges) {
|
||||
if (!window.confirm(t('profileSwitchUnsavedConfirm'))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
onSwitchProfile(value);
|
||||
setIsSaveAsMode(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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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-[360px] bg-background"
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
value={activeProfileId}
|
||||
onValueChange={handleSelectChange}
|
||||
>
|
||||
<SelectTrigger id="profile-selector" className="h-9 rounded-r-none border-r-0 w-[360px] bg-background">
|
||||
<SelectValue>
|
||||
{activeProfile?.name}
|
||||
{hasUnsavedChanges && ' *'}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profiles.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 : onSaveProfile}
|
||||
icon={<Save className="h-4 w-4" />}
|
||||
tooltip={isSaveAsMode ? t('profileSaveAs') : t('profileSave')}
|
||||
disabled={(isSaveAsMode && !newProfileName.trim()) || (!isSaveAsMode && !hasUnsavedChanges)}
|
||||
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}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user