Add new data manager modal with clipboard support and basic json editor

This commit is contained in:
2026-01-21 17:12:47 +00:00
parent 6983ce3853
commit b9a2489225
7 changed files with 911 additions and 321 deletions

View File

@@ -20,8 +20,7 @@ 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, APP_VERSION } from '../constants/defaults';
import { exportSettings, downloadExport, parseImportFile, validateImportData, importSettings } from '../utils/exportImport';
import { getDefaultState } from '../constants/defaults';
/**
* Helper function to create translation interpolation values for defaults.
@@ -120,6 +119,7 @@ const Settings = ({
onUpdateUiSetting,
onReset,
onImportDays,
onOpenDataManagement,
t
}: any) => {
const { showDayTimeOnXAxis, yAxisMin, yAxisMax, showTemplateDay, simulationDays, displayedDays } = uiSettings;
@@ -131,24 +131,6 @@ 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);
@@ -181,7 +163,6 @@ 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);
}
@@ -221,27 +202,22 @@ const Settings = ({
const updateDiagramExpanded = (value: boolean) => {
setIsDiagramExpanded(value);
saveCardStates({ diagram: value, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded, dataManagement: isDataManagementExpanded });
saveCardStates({ diagram: value, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded });
};
const updateSimulationExpanded = (value: boolean) => {
setIsSimulationExpanded(value);
saveCardStates({ diagram: isDiagramExpanded, simulation: value, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded, dataManagement: isDataManagementExpanded });
saveCardStates({ diagram: isDiagramExpanded, simulation: value, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded });
};
const updatePharmacokineticExpanded = (value: boolean) => {
setIsPharmacokineticExpanded(value);
saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: value, advanced: isAdvancedExpanded, dataManagement: isDataManagementExpanded });
saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: value, advanced: isAdvancedExpanded });
};
const updateAdvancedExpanded = (value: boolean) => {
setIsAdvancedExpanded(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 });
saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: value });
};
const saveCardStates = (states: any) => {
@@ -269,115 +245,6 @@ 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);
@@ -1254,187 +1121,15 @@ 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>
{/* Data Management Button */}
<Button
type="button"
onClick={onOpenDataManagement}
variant="outline"
className="w-full"
>
{t('openDataManagement')}
</Button>
{/* Reset Button - Always Visible */}
<Button