Add import/export feature

This commit is contained in:
2026-01-17 14:21:33 +00:00
parent fda0778edb
commit b911fa1e16
5 changed files with 695 additions and 5 deletions

View File

@@ -66,6 +66,7 @@ const MedPlanAssistant = () => {
const {
appState,
updateState,
updateNestedState,
updateUiSetting,
handleReset,
@@ -219,10 +220,13 @@ const MedPlanAssistant = () => {
pkParams={pkParams}
therapeuticRange={therapeuticRange}
uiSettings={uiSettings}
days={days}
doseIncrement={doseIncrement}
onUpdatePkParams={(key: any, value: any) => updateNestedState('pkParams', key, value)}
onUpdateTherapeuticRange={(key: any, value: any) => updateNestedState('therapeuticRange', key, value)}
onUpdateUiSetting={updateUiSetting}
onReset={handleReset}
onImportDays={(importedDays: any) => updateState('days', importedDays)}
t={t}
/>
</div>

View File

@@ -20,7 +20,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
import { FormNumericInput } from './ui/form-numeric-input';
import CollapsibleCardHeader from './ui/collapsible-card-header';
import { Info } from 'lucide-react';
import { getDefaultState } from '../constants/defaults';
import { getDefaultState, APP_VERSION } from '../constants/defaults';
import { exportSettings, downloadExport, parseImportFile, validateImportData, importSettings } from '../utils/exportImport';
/**
* Helper function to create translation interpolation values for defaults.
@@ -110,10 +111,13 @@ const Settings = ({
pkParams,
therapeuticRange,
uiSettings,
days,
doseIncrement,
onUpdatePkParams,
onUpdateTherapeuticRange,
onUpdateUiSetting,
onReset,
onImportDays,
t
}: any) => {
const { showDayTimeOnXAxis, yAxisMin, yAxisMax, showTemplateDay, simulationDays, displayedDays } = uiSettings;
@@ -125,6 +129,24 @@ const Settings = ({
const [isSimulationExpanded, setIsSimulationExpanded] = React.useState(true);
const [isPharmacokineticExpanded, setIsPharmacokineticExpanded] = React.useState(true);
const [isAdvancedExpanded, setIsAdvancedExpanded] = React.useState(false);
const [isDataManagementExpanded, setIsDataManagementExpanded] = React.useState(false);
const [exportOptions, setExportOptions] = React.useState({
includeSchedules: true,
includeDiagramSettings: true,
includeSimulationSettings: true,
includePharmacoSettings: true,
includeAdvancedSettings: false,
});
const [importOptions, setImportOptions] = React.useState({
includeSchedules: true,
includeDiagramSettings: true,
includeSimulationSettings: true,
includePharmacoSettings: true,
includeAdvancedSettings: false,
});
const [selectedFile, setSelectedFile] = React.useState<File | null>(null);
const fileInputRef = React.useRef<HTMLInputElement>(null);
// Track which tooltip is currently open (for mobile touch interaction)
const [openTooltipId, setOpenTooltipId] = React.useState<string | null>(null);
@@ -157,6 +179,7 @@ const Settings = ({
if (states.simulation !== undefined) setIsSimulationExpanded(states.simulation);
if (states.pharmacokinetic !== undefined) setIsPharmacokineticExpanded(states.pharmacokinetic);
if (states.advanced !== undefined) setIsAdvancedExpanded(states.advanced);
if (states.dataManagement !== undefined) setIsDataManagementExpanded(states.dataManagement);
} catch (e) {
console.warn('Failed to load settings card states:', e);
}
@@ -196,22 +219,27 @@ const Settings = ({
const updateDiagramExpanded = (value: boolean) => {
setIsDiagramExpanded(value);
saveCardStates({ diagram: value, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded });
saveCardStates({ diagram: value, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded, dataManagement: isDataManagementExpanded });
};
const updateSimulationExpanded = (value: boolean) => {
setIsSimulationExpanded(value);
saveCardStates({ diagram: isDiagramExpanded, simulation: value, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded });
saveCardStates({ diagram: isDiagramExpanded, simulation: value, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded, dataManagement: isDataManagementExpanded });
};
const updatePharmacokineticExpanded = (value: boolean) => {
setIsPharmacokineticExpanded(value);
saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: value, advanced: isAdvancedExpanded });
saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: value, advanced: isAdvancedExpanded, dataManagement: isDataManagementExpanded });
};
const updateAdvancedExpanded = (value: boolean) => {
setIsAdvancedExpanded(value);
saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: value });
saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: value, dataManagement: isDataManagementExpanded });
};
const updateDataManagementExpanded = (value: boolean) => {
setIsDataManagementExpanded(value);
saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded, dataManagement: value });
};
const saveCardStates = (states: any) => {
@@ -239,6 +267,115 @@ const Settings = ({
});
};
// Export/Import handlers
const handleExport = () => {
const hasAnySelected = Object.values(exportOptions).some(v => v);
if (!hasAnySelected) {
alert(t('exportNoOptionsSelected'));
return;
}
const appState = {
pkParams,
days,
therapeuticRange,
doseIncrement,
uiSettings,
steadyStateConfig: { daysOnMedication: pkParams.advanced.steadyStateDays }
};
const exportData = exportSettings(appState, exportOptions, APP_VERSION);
downloadExport(exportData);
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
setSelectedFile(file || null);
};
const handleImport = async () => {
if (!selectedFile) {
alert(t('importFileNotSelected'));
return;
}
const hasAnySelected = Object.values(importOptions).some(v => v);
if (!hasAnySelected) {
alert(t('importNoOptionsSelected'));
return;
}
try {
const fileContent = await selectedFile.text();
const importData = parseImportFile(fileContent);
if (!importData) {
alert(t('importParseError'));
return;
}
const validation = validateImportData(importData);
if (!validation.isValid) {
alert(t('importError') + '\n\n' + validation.errors.join('\n'));
return;
}
if (validation.warnings.length > 0) {
const warningMessage = t('importValidationTitle') + '\n\n' +
t('importValidationWarnings') + '\n' +
validation.warnings.join('\n') + '\n\n' +
t('importValidationContinue');
if (!window.confirm(warningMessage)) {
return;
}
}
// Apply import
const currentState = { pkParams, days, therapeuticRange, doseIncrement, uiSettings, steadyStateConfig: { daysOnMedication: pkParams.advanced.steadyStateDays } };
const newState = importSettings(currentState, importData.data, importOptions);
// Apply schedules
if (newState.days && importOptions.includeSchedules && onImportDays) {
onImportDays(newState.days);
}
// Apply PK params
if (newState.pkParams && importOptions.includePharmacoSettings) {
Object.entries(newState.pkParams).forEach(([key, value]) => {
if (key !== 'advanced') {
onUpdatePkParams(key, value);
}
});
}
if (newState.pkParams?.advanced && importOptions.includeAdvancedSettings) {
onUpdatePkParams('advanced', newState.pkParams.advanced);
}
if (newState.therapeuticRange && importOptions.includePharmacoSettings) {
Object.entries(newState.therapeuticRange).forEach(([key, value]) => {
onUpdateTherapeuticRange(key, value);
});
}
if (newState.uiSettings) {
Object.entries(newState.uiSettings).forEach(([key, value]) => {
onUpdateUiSetting(key as any, value);
});
}
alert(t('importSuccess'));
setSelectedFile(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
} catch (error) {
console.error('Import error:', error);
alert(t('importError'));
}
};
// Check for out-of-range warnings
const absorptionHL = parseFloat(pkParams.ldx.absorptionHalfLife);
const conversionHL = parseFloat(pkParams.ldx.halfLife);
@@ -975,6 +1112,188 @@ const Settings = ({
)}
</Card>
{/* Export/Import Settings Card */}
<Card>
<CollapsibleCardHeader
title={t('dataManagement')}
isCollapsed={!isDataManagementExpanded}
onToggle={() => updateDataManagementExpanded(!isDataManagementExpanded)}
/>
{isDataManagementExpanded && (
<CardContent className="space-y-4">
<div className="flex items-start gap-2 text-sm text-muted-foreground">
<Info className="h-4 w-4 mt-0.5 shrink-0" />
<p>{t('exportImportTooltip')}</p>
</div>
<Separator className="my-4" />
{/* Export Section */}
<div className="space-y-3">
<h4 className="font-semibold text-sm">{t('exportSettings')}</h4>
<p className="text-xs text-muted-foreground">{t('exportSelectWhat')}</p>
<div className="space-y-2 ml-2">
<div className="flex items-center gap-2">
<Switch
id="export-schedules"
checked={exportOptions.includeSchedules}
onCheckedChange={checked => setExportOptions({...exportOptions, includeSchedules: checked})}
/>
<Label htmlFor="export-schedules" className="text-sm">
{t('exportOptionSchedules')}
</Label>
</div>
<div className="flex items-center gap-2">
<Switch
id="export-diagram"
checked={exportOptions.includeDiagramSettings}
onCheckedChange={checked => setExportOptions({...exportOptions, includeDiagramSettings: checked})}
/>
<Label htmlFor="export-diagram" className="text-sm">
{t('exportOptionDiagram')}
</Label>
</div>
<div className="flex items-center gap-2">
<Switch
id="export-simulation"
checked={exportOptions.includeSimulationSettings}
onCheckedChange={checked => setExportOptions({...exportOptions, includeSimulationSettings: checked})}
/>
<Label htmlFor="export-simulation" className="text-sm">
{t('exportOptionSimulation')}
</Label>
</div>
<div className="flex items-center gap-2">
<Switch
id="export-pharmaco"
checked={exportOptions.includePharmacoSettings}
onCheckedChange={checked => setExportOptions({...exportOptions, includePharmacoSettings: checked})}
/>
<Label htmlFor="export-pharmaco" className="text-sm">
{t('exportOptionPharmaco')}
</Label>
</div>
<div className="flex items-center gap-2">
<Switch
id="export-advanced"
checked={exportOptions.includeAdvancedSettings}
onCheckedChange={checked => setExportOptions({...exportOptions, includeAdvancedSettings: checked})}
/>
<Label htmlFor="export-advanced" className="text-sm">
{t('exportOptionAdvanced')}
</Label>
</div>
</div>
<Button
type="button"
onClick={handleExport}
variant="default"
className="w-full"
>
{t('exportButton')}
</Button>
</div>
<Separator className="my-4" />
{/* Import Section */}
<div className="space-y-3">
<h4 className="font-semibold text-sm">{t('importSettings')}</h4>
<p className="text-xs text-muted-foreground">{t('importSelectWhat')}</p>
<div className="space-y-2 ml-2">
<div className="flex items-center gap-2">
<Switch
id="import-schedules"
checked={importOptions.includeSchedules}
onCheckedChange={checked => setImportOptions({...importOptions, includeSchedules: checked})}
/>
<Label htmlFor="import-schedules" className="text-sm">
{t('exportOptionSchedules')}
</Label>
</div>
<div className="flex items-center gap-2">
<Switch
id="import-diagram"
checked={importOptions.includeDiagramSettings}
onCheckedChange={checked => setImportOptions({...importOptions, includeDiagramSettings: checked})}
/>
<Label htmlFor="import-diagram" className="text-sm">
{t('exportOptionDiagram')}
</Label>
</div>
<div className="flex items-center gap-2">
<Switch
id="import-simulation"
checked={importOptions.includeSimulationSettings}
onCheckedChange={checked => setImportOptions({...importOptions, includeSimulationSettings: checked})}
/>
<Label htmlFor="import-simulation" className="text-sm">
{t('exportOptionSimulation')}
</Label>
</div>
<div className="flex items-center gap-2">
<Switch
id="import-pharmaco"
checked={importOptions.includePharmacoSettings}
onCheckedChange={checked => setImportOptions({...importOptions, includePharmacoSettings: checked})}
/>
<Label htmlFor="import-pharmaco" className="text-sm">
{t('exportOptionPharmaco')}
</Label>
</div>
<div className="flex items-center gap-2">
<Switch
id="import-advanced"
checked={importOptions.includeAdvancedSettings}
onCheckedChange={checked => setImportOptions({...importOptions, includeAdvancedSettings: checked})}
/>
<Label htmlFor="import-advanced" className="text-sm">
{t('exportOptionAdvanced')}
</Label>
</div>
</div>
<div className="space-y-2">
<input
ref={fileInputRef}
type="file"
accept=".json"
onChange={handleFileSelect}
className="hidden"
id="import-file-input"
/>
<Button
type="button"
onClick={() => fileInputRef.current?.click()}
variant="outline"
className="w-full"
>
{t('importButton')}
</Button>
{selectedFile && (
<p className="text-xs text-muted-foreground">
{t('importFileSelected')} <span className="font-mono">{selectedFile.name}</span>
</p>
)}
{selectedFile && (
<Button
type="button"
onClick={handleImport}
variant="default"
className="w-full"
>
{t('importApplyButton')}
</Button>
)}
</div>
</div>
</CardContent>
)}
</Card>
{/* Reset Button - Always Visible */}
<Button
type="button"

View File

@@ -166,6 +166,34 @@ export const de = {
// Reset confirmation
resetConfirmation: "Bist du sicher, dass du alle Einstellungen auf die Standardwerte zurücksetzen möchtest? Dies kann nicht rückgängig gemacht werden.",
// Export/Import
dataManagement: "Datenverwaltung",
exportSettings: "Einstellungen exportieren",
importSettings: "Einstellungen importieren",
exportSelectWhat: "Was möchtest du exportieren:",
importSelectWhat: "Was möchtest du importieren:",
exportOptionSchedules: "Zeitpläne (Tagespläne mit Dosen)",
exportOptionDiagram: "Diagramm-Einstellungen (Ansichtsoptionen, Diagrammanzeige)",
exportOptionSimulation: "Simulations-Einstellungen (Dauer, Bereich, Diagrammansicht)",
exportOptionPharmaco: "Pharmakokinetik-Einstellungen (Halbwertszeiten, therapeutischer Bereich)",
exportOptionAdvanced: "Erweiterte Einstellungen (Gewicht, Nahrung, pH, Bioverfügbarkeit)",
exportButton: "Backup-Datei herunterladen",
importButton: "Datei zum Importieren wählen",
importApplyButton: "Import anwenden",
importCancelButton: "Abbrechen",
importValidationTitle: "Import-Validierung",
importValidationWarnings: "Warnungen:",
importValidationErrors: "Fehler:",
importValidationContinue: "Möchtest du mit dem Import fortfahren?",
importSuccess: "Einstellungen erfolgreich importiert!",
importError: "Import fehlgeschlagen. Bitte überprüfe das Dateiformat.",
importParseError: "Datei konnte nicht gelesen werden. Stelle sicher, dass es eine gültige JSON-Backup-Datei ist.",
importNoOptionsSelected: "Bitte wähle mindestens eine Kategorie zum Importieren aus.",
exportNoOptionsSelected: "Bitte wähle mindestens eine Kategorie zum Exportieren aus.",
importFileSelected: "Datei ausgewählt:",
importFileNotSelected: "Keine Datei ausgewählt",
exportImportTooltip: "Exportiere deine Einstellungen als Backup oder zum Teilen. Importiere zuvor exportierte Einstellungen. Wähle individuell, welche Teile exportiert/importiert werden sollen.",
// Footer disclaimer
importantNote: "Wichtiger Hinweis",
disclaimer: "Dieses Tool dient ausschließlich zu Illustrations- und Informationszwecken. Es ist kein medizinisches Gerät und ersetzt nicht die Beratung durch einen Arzt oder Apotheker. Alle Berechnungen sind Simulationen, die auf allgemeinen pharmakokinetischen Modellen basieren und von individuellen Faktoren erheblich abweichen können. Bitte konsultiere deinen behandelnden Arzt, bevor du Anpassungen an deiner Medikation vornimmst.",

View File

@@ -164,6 +164,34 @@ export const en = {
// Reset confirmation
resetConfirmation: "Are you sure you want to reset all settings to default values? This cannot be undone.",
// Export/Import
dataManagement: "Data Management",
exportSettings: "Export Settings",
importSettings: "Import Settings",
exportSelectWhat: "Select what to export:",
importSelectWhat: "Select what to import:",
exportOptionSchedules: "Schedules (Day plans with doses)",
exportOptionDiagram: "Diagram Settings (View options, chart display)",
exportOptionSimulation: "Simulation Settings (Duration, range, chart view)",
exportOptionPharmaco: "Pharmacokinetic Settings (Half-lives, therapeutic range)",
exportOptionAdvanced: "Advanced Settings (Weight, food, pH, bioavailability)",
exportButton: "Download Backup File",
importButton: "Choose File to Import",
importApplyButton: "Apply Import",
importCancelButton: "Cancel",
importValidationTitle: "Import Validation",
importValidationWarnings: "Warnings:",
importValidationErrors: "Errors:",
importValidationContinue: "Do you want to continue with the import?",
importSuccess: "Settings imported successfully!",
importError: "Import failed. Please check the file format.",
importParseError: "Failed to read file. Please ensure it's a valid JSON backup file.",
importNoOptionsSelected: "Please select at least one category to import.",
exportNoOptionsSelected: "Please select at least one category to export.",
importFileSelected: "File selected:",
importFileNotSelected: "No file selected",
exportImportTooltip: "Export your settings as backup or share with others. Import previously exported settings. Choose which parts to export/import individually.",
// Footer disclaimer
importantNote: "Important Notice",
disclaimer: "This tool is for illustration and information purposes only. It is not a medical device and does not replace consultation with a doctor or pharmacist. All calculations are simulations based on general pharmacokinetic models and may differ significantly from individual factors. Please consult your treating physician before making adjustments to your medication.",

311
src/utils/exportImport.ts Normal file
View File

@@ -0,0 +1,311 @@
/**
* Export/Import Utility
*
* Handles selective export and import of application settings with
* validation, versioning, and graceful error handling.
*
* @author Andreas Weyer
* @license MIT
*/
import { AppState, getDefaultState } from '../constants/defaults';
export interface ExportData {
version: string;
exportDate: string;
appVersion: string;
data: {
schedules?: AppState['days'];
diagramSettings?: {
showDayTimeOnXAxis: AppState['uiSettings']['showDayTimeOnXAxis'];
showTemplateDay: AppState['uiSettings']['showTemplateDay'];
showDayReferenceLines: AppState['uiSettings']['showDayReferenceLines'];
showTherapeuticRange: AppState['uiSettings']['showTherapeuticRange'];
stickyChart: AppState['uiSettings']['stickyChart'];
};
simulationSettings?: {
simulationDays: AppState['uiSettings']['simulationDays'];
displayedDays: AppState['uiSettings']['displayedDays'];
yAxisMin: AppState['uiSettings']['yAxisMin'];
yAxisMax: AppState['uiSettings']['yAxisMax'];
chartView: AppState['uiSettings']['chartView'];
steadyStateDaysEnabled: AppState['uiSettings']['steadyStateDaysEnabled'];
};
pharmacoSettings?: {
pkParams: Omit<AppState['pkParams'], 'advanced'>;
therapeuticRange: AppState['therapeuticRange'];
doseIncrement: AppState['doseIncrement'];
};
advancedSettings?: AppState['pkParams']['advanced'];
};
}
export interface ExportOptions {
includeSchedules: boolean;
includeDiagramSettings: boolean;
includeSimulationSettings: boolean;
includePharmacoSettings: boolean;
includeAdvancedSettings: boolean;
}
export interface ImportValidationResult {
isValid: boolean;
warnings: string[];
errors: string[];
hasUnknownFields: boolean;
hasMissingFields: boolean;
}
const EXPORT_FORMAT_VERSION = '1.0';
/**
* Export selected settings to a JSON structure
*/
export const exportSettings = (
appState: AppState,
options: ExportOptions,
appVersion: string
): ExportData => {
const exportData: ExportData = {
version: EXPORT_FORMAT_VERSION,
exportDate: new Date().toISOString(),
appVersion,
data: {}
};
if (options.includeSchedules) {
exportData.data.schedules = appState.days;
}
if (options.includeDiagramSettings) {
exportData.data.diagramSettings = {
showDayTimeOnXAxis: appState.uiSettings.showDayTimeOnXAxis,
showTemplateDay: appState.uiSettings.showTemplateDay,
showDayReferenceLines: appState.uiSettings.showDayReferenceLines ?? true,
showTherapeuticRange: appState.uiSettings.showTherapeuticRange ?? true,
stickyChart: appState.uiSettings.stickyChart,
};
}
if (options.includeSimulationSettings) {
exportData.data.simulationSettings = {
simulationDays: appState.uiSettings.simulationDays,
displayedDays: appState.uiSettings.displayedDays,
yAxisMin: appState.uiSettings.yAxisMin,
yAxisMax: appState.uiSettings.yAxisMax,
chartView: appState.uiSettings.chartView,
steadyStateDaysEnabled: appState.uiSettings.steadyStateDaysEnabled ?? true,
};
}
if (options.includePharmacoSettings) {
const { advanced, ...pkParamsWithoutAdvanced } = appState.pkParams;
exportData.data.pharmacoSettings = {
pkParams: pkParamsWithoutAdvanced as any,
therapeuticRange: appState.therapeuticRange,
doseIncrement: appState.doseIncrement,
};
}
if (options.includeAdvancedSettings) {
exportData.data.advancedSettings = appState.pkParams.advanced;
}
return exportData;
};
/**
* Download export data as JSON file
*/
export const downloadExport = (exportData: ExportData, filename?: string) => {
const jsonString = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename || `med-plan-backup-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
/**
* Validate import data structure and content
*/
export const validateImportData = (data: any): ImportValidationResult => {
const result: ImportValidationResult = {
isValid: true,
warnings: [],
errors: [],
hasUnknownFields: false,
hasMissingFields: false,
};
// Check if data is an object
if (!data || typeof data !== 'object') {
result.isValid = false;
result.errors.push('Invalid file format: Not a valid JSON object');
return result;
}
// Check version
if (!data.version) {
result.warnings.push('No version information found - this may be from an older export format');
} else if (data.version !== EXPORT_FORMAT_VERSION) {
result.warnings.push(`Version mismatch: Export is v${data.version}, current format is v${EXPORT_FORMAT_VERSION}`);
}
// Check if data section exists
if (!data.data || typeof data.data !== 'object') {
result.isValid = false;
result.errors.push('Invalid file format: Missing data section');
return result;
}
const importData = data.data;
// Validate schedules
if (importData.schedules !== undefined) {
if (!Array.isArray(importData.schedules)) {
result.errors.push('Schedules: Invalid format (expected array)');
result.isValid = false;
} else {
// Check for required fields in schedules
importData.schedules.forEach((day: any, index: number) => {
if (!day.id || !Array.isArray(day.doses)) {
result.warnings.push(`Schedule day ${index + 1}: Missing required fields`);
result.hasMissingFields = true;
}
day.doses?.forEach((dose: any, doseIndex: number) => {
if (!dose.id || dose.time === undefined || dose.ldx === undefined) {
result.warnings.push(`Schedule day ${index + 1}, dose ${doseIndex + 1}: Missing required fields`);
result.hasMissingFields = true;
}
});
});
}
}
// Validate diagram settings
if (importData.diagramSettings !== undefined) {
const validFields = ['showDayTimeOnXAxis', 'showTemplateDay', 'showDayReferenceLines', 'showTherapeuticRange', 'stickyChart'];
const importedFields = Object.keys(importData.diagramSettings);
const unknownFields = importedFields.filter(f => !validFields.includes(f));
if (unknownFields.length > 0) {
result.warnings.push(`Diagram settings: Unknown fields found (${unknownFields.join(', ')})`);
result.hasUnknownFields = true;
}
}
// Validate simulation settings
if (importData.simulationSettings !== undefined) {
const validFields = ['simulationDays', 'displayedDays', 'yAxisMin', 'yAxisMax', 'chartView', 'steadyStateDaysEnabled'];
const importedFields = Object.keys(importData.simulationSettings);
const unknownFields = importedFields.filter(f => !validFields.includes(f));
if (unknownFields.length > 0) {
result.warnings.push(`Simulation settings: Unknown fields found (${unknownFields.join(', ')})`);
result.hasUnknownFields = true;
}
}
// Validate pharmaco settings
if (importData.pharmacoSettings !== undefined) {
if (!importData.pharmacoSettings.pkParams) {
result.warnings.push('Pharmaco settings: Missing PK parameters');
result.hasMissingFields = true;
}
}
// Validate advanced settings
if (importData.advancedSettings !== undefined) {
const validCategories = ['weightBasedVd', 'foodEffect', 'urinePh', 'fOral', 'steadyStateDays'];
const importedCategories = Object.keys(importData.advancedSettings);
const unknownCategories = importedCategories.filter(c => !validCategories.includes(c));
if (unknownCategories.length > 0) {
result.warnings.push(`Advanced settings: Unknown fields found (${unknownCategories.join(', ')})`);
result.hasUnknownFields = true;
}
}
return result;
};
/**
* Import validated data into app state
*/
export const importSettings = (
currentState: AppState,
importData: ExportData['data'],
options: ExportOptions
): Partial<AppState> => {
const newState: Partial<AppState> = {};
if (options.includeSchedules && importData.schedules) {
newState.days = importData.schedules.map(day => ({
...day,
// Ensure all required fields exist
doses: day.doses.map(dose => ({
id: dose.id || `dose-${Date.now()}-${Math.random()}`,
time: dose.time || '12:00',
ldx: dose.ldx || '0',
damph: dose.damph,
}))
}));
}
if (options.includeDiagramSettings && importData.diagramSettings) {
if (!newState.uiSettings) {
newState.uiSettings = { ...currentState.uiSettings };
}
Object.assign(newState.uiSettings, importData.diagramSettings);
}
if (options.includeSimulationSettings && importData.simulationSettings) {
if (!newState.uiSettings) {
newState.uiSettings = { ...currentState.uiSettings };
}
Object.assign(newState.uiSettings, importData.simulationSettings);
}
if (options.includePharmacoSettings && importData.pharmacoSettings) {
if (importData.pharmacoSettings.pkParams) {
newState.pkParams = {
...currentState.pkParams,
...importData.pharmacoSettings.pkParams,
advanced: currentState.pkParams.advanced, // Keep current advanced settings
};
}
if (importData.pharmacoSettings.therapeuticRange) {
newState.therapeuticRange = importData.pharmacoSettings.therapeuticRange;
}
if (importData.pharmacoSettings.doseIncrement !== undefined) {
newState.doseIncrement = importData.pharmacoSettings.doseIncrement;
}
}
if (options.includeAdvancedSettings && importData.advancedSettings) {
if (!newState.pkParams) {
newState.pkParams = { ...currentState.pkParams };
}
newState.pkParams.advanced = {
...currentState.pkParams.advanced,
...importData.advancedSettings,
};
}
return newState;
};
/**
* Parse JSON file content
*/
export const parseImportFile = (fileContent: string): ExportData | null => {
try {
const data = JSON.parse(fileContent);
return data;
} catch (error) {
console.error('Failed to parse import file:', error);
return null;
}
};