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:
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user