Add import/export feature
This commit is contained in:
@@ -66,6 +66,7 @@ const MedPlanAssistant = () => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
appState,
|
appState,
|
||||||
|
updateState,
|
||||||
updateNestedState,
|
updateNestedState,
|
||||||
updateUiSetting,
|
updateUiSetting,
|
||||||
handleReset,
|
handleReset,
|
||||||
@@ -219,10 +220,13 @@ const MedPlanAssistant = () => {
|
|||||||
pkParams={pkParams}
|
pkParams={pkParams}
|
||||||
therapeuticRange={therapeuticRange}
|
therapeuticRange={therapeuticRange}
|
||||||
uiSettings={uiSettings}
|
uiSettings={uiSettings}
|
||||||
|
days={days}
|
||||||
|
doseIncrement={doseIncrement}
|
||||||
onUpdatePkParams={(key: any, value: any) => updateNestedState('pkParams', key, value)}
|
onUpdatePkParams={(key: any, value: any) => updateNestedState('pkParams', key, value)}
|
||||||
onUpdateTherapeuticRange={(key: any, value: any) => updateNestedState('therapeuticRange', key, value)}
|
onUpdateTherapeuticRange={(key: any, value: any) => updateNestedState('therapeuticRange', key, value)}
|
||||||
onUpdateUiSetting={updateUiSetting}
|
onUpdateUiSetting={updateUiSetting}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
|
onImportDays={(importedDays: any) => updateState('days', importedDays)}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
|
|||||||
import { FormNumericInput } from './ui/form-numeric-input';
|
import { FormNumericInput } from './ui/form-numeric-input';
|
||||||
import CollapsibleCardHeader from './ui/collapsible-card-header';
|
import CollapsibleCardHeader from './ui/collapsible-card-header';
|
||||||
import { Info } from 'lucide-react';
|
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.
|
* Helper function to create translation interpolation values for defaults.
|
||||||
@@ -110,10 +111,13 @@ const Settings = ({
|
|||||||
pkParams,
|
pkParams,
|
||||||
therapeuticRange,
|
therapeuticRange,
|
||||||
uiSettings,
|
uiSettings,
|
||||||
|
days,
|
||||||
|
doseIncrement,
|
||||||
onUpdatePkParams,
|
onUpdatePkParams,
|
||||||
onUpdateTherapeuticRange,
|
onUpdateTherapeuticRange,
|
||||||
onUpdateUiSetting,
|
onUpdateUiSetting,
|
||||||
onReset,
|
onReset,
|
||||||
|
onImportDays,
|
||||||
t
|
t
|
||||||
}: any) => {
|
}: any) => {
|
||||||
const { showDayTimeOnXAxis, yAxisMin, yAxisMax, showTemplateDay, simulationDays, displayedDays } = uiSettings;
|
const { showDayTimeOnXAxis, yAxisMin, yAxisMax, showTemplateDay, simulationDays, displayedDays } = uiSettings;
|
||||||
@@ -125,6 +129,24 @@ const Settings = ({
|
|||||||
const [isSimulationExpanded, setIsSimulationExpanded] = React.useState(true);
|
const [isSimulationExpanded, setIsSimulationExpanded] = React.useState(true);
|
||||||
const [isPharmacokineticExpanded, setIsPharmacokineticExpanded] = React.useState(true);
|
const [isPharmacokineticExpanded, setIsPharmacokineticExpanded] = React.useState(true);
|
||||||
const [isAdvancedExpanded, setIsAdvancedExpanded] = React.useState(false);
|
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)
|
// Track which tooltip is currently open (for mobile touch interaction)
|
||||||
const [openTooltipId, setOpenTooltipId] = React.useState<string | null>(null);
|
const [openTooltipId, setOpenTooltipId] = React.useState<string | null>(null);
|
||||||
@@ -157,6 +179,7 @@ const Settings = ({
|
|||||||
if (states.simulation !== undefined) setIsSimulationExpanded(states.simulation);
|
if (states.simulation !== undefined) setIsSimulationExpanded(states.simulation);
|
||||||
if (states.pharmacokinetic !== undefined) setIsPharmacokineticExpanded(states.pharmacokinetic);
|
if (states.pharmacokinetic !== undefined) setIsPharmacokineticExpanded(states.pharmacokinetic);
|
||||||
if (states.advanced !== undefined) setIsAdvancedExpanded(states.advanced);
|
if (states.advanced !== undefined) setIsAdvancedExpanded(states.advanced);
|
||||||
|
if (states.dataManagement !== undefined) setIsDataManagementExpanded(states.dataManagement);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to load settings card states:', e);
|
console.warn('Failed to load settings card states:', e);
|
||||||
}
|
}
|
||||||
@@ -196,22 +219,27 @@ const Settings = ({
|
|||||||
|
|
||||||
const updateDiagramExpanded = (value: boolean) => {
|
const updateDiagramExpanded = (value: boolean) => {
|
||||||
setIsDiagramExpanded(value);
|
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) => {
|
const updateSimulationExpanded = (value: boolean) => {
|
||||||
setIsSimulationExpanded(value);
|
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) => {
|
const updatePharmacokineticExpanded = (value: boolean) => {
|
||||||
setIsPharmacokineticExpanded(value);
|
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) => {
|
const updateAdvancedExpanded = (value: boolean) => {
|
||||||
setIsAdvancedExpanded(value);
|
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) => {
|
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
|
// Check for out-of-range warnings
|
||||||
const absorptionHL = parseFloat(pkParams.ldx.absorptionHalfLife);
|
const absorptionHL = parseFloat(pkParams.ldx.absorptionHalfLife);
|
||||||
const conversionHL = parseFloat(pkParams.ldx.halfLife);
|
const conversionHL = parseFloat(pkParams.ldx.halfLife);
|
||||||
@@ -975,6 +1112,188 @@ const Settings = ({
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</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 */}
|
{/* Reset Button - Always Visible */}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -166,6 +166,34 @@ export const de = {
|
|||||||
// Reset confirmation
|
// 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.",
|
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
|
// Footer disclaimer
|
||||||
importantNote: "Wichtiger Hinweis",
|
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.",
|
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.",
|
||||||
|
|||||||
@@ -164,6 +164,34 @@ export const en = {
|
|||||||
// Reset confirmation
|
// Reset confirmation
|
||||||
resetConfirmation: "Are you sure you want to reset all settings to default values? This cannot be undone.",
|
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
|
// Footer disclaimer
|
||||||
importantNote: "Important Notice",
|
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.",
|
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
311
src/utils/exportImport.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user