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:
2026-02-10 18:33:41 +00:00
parent 3b4db14424
commit b198164760
8 changed files with 1000 additions and 177 deletions

View File

@@ -10,7 +10,7 @@
*/
import React from 'react';
import { LOCAL_STORAGE_KEY, getDefaultState, MAX_DOSES_PER_DAY, type AppState, type DayGroup, type DayDose } from '../constants/defaults';
import { LOCAL_STORAGE_KEY, getDefaultState, MAX_DOSES_PER_DAY, MAX_PROFILES, type AppState, type DayGroup, type DayDose, type ScheduleProfile } from '../constants/defaults';
export const useAppState = () => {
const [appState, setAppState] = React.useState<AppState>(getDefaultState);
@@ -94,51 +94,45 @@ export const useAppState = () => {
return value;
};
// Validate basic pkParams
if (migratedPkParams.basic) {
migratedPkParams.basic.eliminationHalfLife = validateNumericField(
migratedPkParams.basic.eliminationHalfLife,
defaults.pkParams.basic.eliminationHalfLife
);
migratedPkParams.basic.bodyWeight = validateNumericField(
migratedPkParams.basic.bodyWeight,
defaults.pkParams.basic.bodyWeight
);
}
// Migrate from old days-only format to profile-based format
let migratedProfiles: ScheduleProfile[] = defaults.profiles;
let migratedActiveProfileId: string = defaults.activeProfileId;
let migratedDays: DayGroup[] = defaults.days;
// Validate advanced pkParams
if (migratedPkParams.advanced) {
migratedPkParams.advanced.conversionEfficiency = validateNumericField(
migratedPkParams.advanced.conversionEfficiency,
defaults.pkParams.advanced.conversionEfficiency
);
migratedPkParams.advanced.bioavailability = validateNumericField(
migratedPkParams.advanced.bioavailability,
defaults.pkParams.advanced.bioavailability
);
migratedPkParams.advanced.customVolumeOfDistribution = validateNumericField(
migratedPkParams.advanced.customVolumeOfDistribution,
defaults.pkParams.advanced.customVolumeOfDistribution
);
migratedPkParams.advanced.absorptionDelay = validateNumericField(
migratedPkParams.advanced.absorptionDelay,
defaults.pkParams.advanced.absorptionDelay
);
migratedPkParams.advanced.absorptionRateConstant = validateNumericField(
migratedPkParams.advanced.absorptionRateConstant,
defaults.pkParams.advanced.absorptionRateConstant
);
migratedPkParams.advanced.mealDelayFactor = validateNumericField(
migratedPkParams.advanced.mealDelayFactor,
defaults.pkParams.advanced.mealDelayFactor
);
if (parsedState.profiles && Array.isArray(parsedState.profiles)) {
// New format with profiles
migratedProfiles = parsedState.profiles;
migratedActiveProfileId = parsedState.activeProfileId || parsedState.profiles[0]?.id || defaults.activeProfileId;
// Validate activeProfileId exists in profiles
const activeProfile = migratedProfiles.find(p => p.id === migratedActiveProfileId);
if (!activeProfile && migratedProfiles.length > 0) {
migratedActiveProfileId = migratedProfiles[0].id;
}
// Set days from active profile
migratedDays = activeProfile?.days || defaults.days;
} else if (parsedState.days) {
// Old format: migrate days to default profile
const now = new Date().toISOString();
migratedProfiles = [{
id: `profile-migrated-${Date.now()}`,
name: 'Default',
days: parsedState.days,
createdAt: now,
modifiedAt: now
}];
migratedActiveProfileId = migratedProfiles[0].id;
migratedDays = parsedState.days;
}
setAppState({
...defaults,
...parsedState,
pkParams: migratedPkParams,
days: parsedState.days || defaults.days,
days: migratedDays,
profiles: migratedProfiles,
activeProfileId: migratedActiveProfileId,
uiSettings: migratedUiSettings,
});
}
@@ -154,6 +148,8 @@ export const useAppState = () => {
const stateToSave = {
pkParams: appState.pkParams,
days: appState.days,
profiles: appState.profiles,
activeProfileId: appState.activeProfileId,
steadyStateConfig: appState.steadyStateConfig,
therapeuticRange: appState.therapeuticRange,
doseIncrement: appState.doseIncrement,
@@ -364,6 +360,153 @@ export const useAppState = () => {
}));
};
// Profile management functions
const getActiveProfile = (): ScheduleProfile | undefined => {
return appState.profiles.find(p => p.id === appState.activeProfileId);
};
const createProfile = (name: string, cloneFromId?: string): string | null => {
if (appState.profiles.length >= MAX_PROFILES) {
console.warn(`Cannot create profile: Maximum of ${MAX_PROFILES} profiles reached`);
return null;
}
const now = new Date().toISOString();
const newProfileId = `profile-${Date.now()}`;
let days: DayGroup[];
if (cloneFromId) {
const sourceProfile = appState.profiles.find(p => p.id === cloneFromId);
days = sourceProfile ? JSON.parse(JSON.stringify(sourceProfile.days)) : appState.days;
} else {
// Create with current days
days = JSON.parse(JSON.stringify(appState.days));
}
// Regenerate IDs for cloned days/doses
days = days.map(day => ({
...day,
id: `day-${Date.now()}-${Math.random()}`,
doses: day.doses.map(dose => ({
...dose,
id: `dose-${Date.now()}-${Math.random()}`
}))
}));
const newProfile: ScheduleProfile = {
id: newProfileId,
name,
days,
createdAt: now,
modifiedAt: now
};
setAppState(prev => ({
...prev,
profiles: [...prev.profiles, newProfile]
}));
return newProfileId;
};
const deleteProfile = (profileId: string): boolean => {
if (appState.profiles.length <= 1) {
console.warn('Cannot delete last profile');
return false;
}
const profileIndex = appState.profiles.findIndex(p => p.id === profileId);
if (profileIndex === -1) {
console.warn('Profile not found');
return false;
}
setAppState(prev => {
const newProfiles = prev.profiles.filter(p => p.id !== profileId);
// If we're deleting the active profile, switch to first remaining profile
let newActiveProfileId = prev.activeProfileId;
if (profileId === prev.activeProfileId) {
newActiveProfileId = newProfiles[0].id;
}
return {
...prev,
profiles: newProfiles,
activeProfileId: newActiveProfileId,
days: newProfiles.find(p => p.id === newActiveProfileId)?.days || prev.days
};
});
return true;
};
const switchProfile = (profileId: string) => {
const profile = appState.profiles.find(p => p.id === profileId);
if (!profile) {
console.warn('Profile not found');
return;
}
setAppState(prev => ({
...prev,
activeProfileId: profileId,
days: profile.days
}));
};
const saveProfile = () => {
const now = new Date().toISOString();
setAppState(prev => ({
...prev,
profiles: prev.profiles.map(p =>
p.id === prev.activeProfileId
? { ...p, days: JSON.parse(JSON.stringify(prev.days)), modifiedAt: now }
: p
)
}));
};
const saveProfileAs = (newName: string): string | null => {
const newProfileId = createProfile(newName, undefined);
if (newProfileId) {
// Save current days to the new profile and switch to it
const now = new Date().toISOString();
setAppState(prev => ({
...prev,
profiles: prev.profiles.map(p =>
p.id === newProfileId
? { ...p, days: JSON.parse(JSON.stringify(prev.days)), modifiedAt: now }
: p
),
activeProfileId: newProfileId
}));
}
return newProfileId;
};
const updateProfileName = (profileId: string, newName: string) => {
setAppState(prev => ({
...prev,
profiles: prev.profiles.map(p =>
p.id === profileId
? { ...p, name: newName, modifiedAt: new Date().toISOString() }
: p
)
}));
};
const hasUnsavedChanges = (): boolean => {
const activeProfile = getActiveProfile();
if (!activeProfile) return false;
return JSON.stringify(activeProfile.days) !== JSON.stringify(appState.days);
};
return {
appState,
isLoaded,
@@ -377,6 +520,15 @@ export const useAppState = () => {
removeDoseFromDay,
updateDoseInDay,
updateDoseFieldInDay,
sortDosesInDay
sortDosesInDay,
// Profile management
getActiveProfile,
createProfile,
deleteProfile,
switchProfile,
saveProfile,
saveProfileAs,
updateProfileName,
hasUnsavedChanges
};
};