diff --git a/src/App.tsx b/src/App.tsx index 51a74d8..2cafaff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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} /> diff --git a/src/components/settings.tsx b/src/components/settings.tsx index 198be09..af7634b 100644 --- a/src/components/settings.tsx +++ b/src/components/settings.tsx @@ -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(null); + const fileInputRef = React.useRef(null); // Track which tooltip is currently open (for mobile touch interaction) const [openTooltipId, setOpenTooltipId] = React.useState(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) => { + 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 = ({ )} + {/* Export/Import Settings Card */} + + updateDataManagementExpanded(!isDataManagementExpanded)} + /> + {isDataManagementExpanded && ( + +
+ +

{t('exportImportTooltip')}

+
+ + + + {/* Export Section */} +
+

{t('exportSettings')}

+

{t('exportSelectWhat')}

+ +
+
+ setExportOptions({...exportOptions, includeSchedules: checked})} + /> + +
+
+ setExportOptions({...exportOptions, includeDiagramSettings: checked})} + /> + +
+
+ setExportOptions({...exportOptions, includeSimulationSettings: checked})} + /> + +
+
+ setExportOptions({...exportOptions, includePharmacoSettings: checked})} + /> + +
+
+ setExportOptions({...exportOptions, includeAdvancedSettings: checked})} + /> + +
+
+ + +
+ + + + {/* Import Section */} +
+

{t('importSettings')}

+

{t('importSelectWhat')}

+ +
+
+ setImportOptions({...importOptions, includeSchedules: checked})} + /> + +
+
+ setImportOptions({...importOptions, includeDiagramSettings: checked})} + /> + +
+
+ setImportOptions({...importOptions, includeSimulationSettings: checked})} + /> + +
+
+ setImportOptions({...importOptions, includePharmacoSettings: checked})} + /> + +
+
+ setImportOptions({...importOptions, includeAdvancedSettings: checked})} + /> + +
+
+ +
+ + + {selectedFile && ( +

+ {t('importFileSelected')} {selectedFile.name} +

+ )} + {selectedFile && ( + + )} +
+
+
+ )} +
+ {/* Reset Button - Always Visible */}