Add new data manager modal with clipboard support and basic json editor
This commit is contained in:
21
src/App.tsx
21
src/App.tsx
@@ -18,6 +18,7 @@ import SimulationChart from './components/simulation-chart';
|
|||||||
import Settings from './components/settings';
|
import Settings from './components/settings';
|
||||||
import LanguageSelector from './components/language-selector';
|
import LanguageSelector from './components/language-selector';
|
||||||
import DisclaimerModal from './components/disclaimer-modal';
|
import DisclaimerModal from './components/disclaimer-modal';
|
||||||
|
import DataManagementModal from './components/data-management-modal';
|
||||||
import { Button } from './components/ui/button';
|
import { Button } from './components/ui/button';
|
||||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from './components/ui/tooltip';
|
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from './components/ui/tooltip';
|
||||||
import { IconButtonWithTooltip } from './components/ui/icon-button-with-tooltip';
|
import { IconButtonWithTooltip } from './components/ui/icon-button-with-tooltip';
|
||||||
@@ -35,6 +36,9 @@ const MedPlanAssistant = () => {
|
|||||||
// Disclaimer modal state
|
// Disclaimer modal state
|
||||||
const [showDisclaimer, setShowDisclaimer] = React.useState(false);
|
const [showDisclaimer, setShowDisclaimer] = React.useState(false);
|
||||||
|
|
||||||
|
// Data management modal state
|
||||||
|
const [showDataManagement, setShowDataManagement] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const hasAccepted = localStorage.getItem('medPlanDisclaimerAccepted_v1');
|
const hasAccepted = localStorage.getItem('medPlanDisclaimerAccepted_v1');
|
||||||
if (!hasAccepted) {
|
if (!hasAccepted) {
|
||||||
@@ -115,6 +119,22 @@ const MedPlanAssistant = () => {
|
|||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Data Management Modal */}
|
||||||
|
<DataManagementModal
|
||||||
|
isOpen={showDataManagement}
|
||||||
|
onClose={() => setShowDataManagement(false)}
|
||||||
|
t={t}
|
||||||
|
pkParams={pkParams}
|
||||||
|
days={days}
|
||||||
|
therapeuticRange={therapeuticRange}
|
||||||
|
doseIncrement={doseIncrement}
|
||||||
|
uiSettings={uiSettings}
|
||||||
|
onUpdatePkParams={(key: any, value: any) => updateNestedState('pkParams', key, value)}
|
||||||
|
onUpdateTherapeuticRange={(key: any, value: any) => updateNestedState('therapeuticRange', key, value)}
|
||||||
|
onUpdateUiSetting={(key: any, value: any) => updateUiSetting(key as any, value)}
|
||||||
|
onImportDays={(importedDays: any) => updateState('days', importedDays)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<header className="mb-8">
|
<header className="mb-8">
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
@@ -229,6 +249,7 @@ const MedPlanAssistant = () => {
|
|||||||
onUpdateUiSetting={updateUiSetting}
|
onUpdateUiSetting={updateUiSetting}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
onImportDays={(importedDays: any) => updateState('days', importedDays)}
|
onImportDays={(importedDays: any) => updateState('days', importedDays)}
|
||||||
|
onOpenDataManagement={() => setShowDataManagement(true)}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
799
src/components/data-management-modal.tsx
Normal file
799
src/components/data-management-modal.tsx
Normal file
@@ -0,0 +1,799 @@
|
|||||||
|
/**
|
||||||
|
* Data Management Modal Component
|
||||||
|
*
|
||||||
|
* Provides a comprehensive interface for exporting and importing application data.
|
||||||
|
* Features include:
|
||||||
|
* - File-based download/upload
|
||||||
|
* - Clipboard copy/paste functionality
|
||||||
|
* - JSON editor for manual editing
|
||||||
|
* - Validation and error handling
|
||||||
|
* - Category selection for partial exports/imports
|
||||||
|
*
|
||||||
|
* @author Andreas Weyer
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
|
import { Label } from './ui/label';
|
||||||
|
import { Switch } from './ui/switch';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { Textarea } from './ui/textarea';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from './ui/popover';
|
||||||
|
import {
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
Copy,
|
||||||
|
ClipboardPaste,
|
||||||
|
Check,
|
||||||
|
X,
|
||||||
|
ChevronDown,
|
||||||
|
FileJson,
|
||||||
|
Info,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
exportSettings,
|
||||||
|
downloadExport,
|
||||||
|
parseImportFile,
|
||||||
|
validateImportData,
|
||||||
|
importSettings,
|
||||||
|
} from '../utils/exportImport';
|
||||||
|
import { APP_VERSION } from '../constants/defaults';
|
||||||
|
|
||||||
|
interface ExportImportOptions {
|
||||||
|
includeSchedules: boolean;
|
||||||
|
includeDiagramSettings: boolean;
|
||||||
|
includeSimulationSettings: boolean;
|
||||||
|
includePharmacoSettings: boolean;
|
||||||
|
includeAdvancedSettings: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DataManagementModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
t: (key: string) => string;
|
||||||
|
// App state
|
||||||
|
pkParams: any;
|
||||||
|
days: any;
|
||||||
|
therapeuticRange: any;
|
||||||
|
doseIncrement: any;
|
||||||
|
uiSettings: any;
|
||||||
|
// Callbacks
|
||||||
|
onUpdatePkParams: (key: string, value: any) => void;
|
||||||
|
onUpdateTherapeuticRange: (key: string, value: any) => void;
|
||||||
|
onUpdateUiSetting: (key: string, value: any) => void;
|
||||||
|
onImportDays?: (days: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DataManagementModal: React.FC<DataManagementModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
t,
|
||||||
|
pkParams,
|
||||||
|
days,
|
||||||
|
therapeuticRange,
|
||||||
|
doseIncrement,
|
||||||
|
uiSettings,
|
||||||
|
onUpdatePkParams,
|
||||||
|
onUpdateTherapeuticRange,
|
||||||
|
onUpdateUiSetting,
|
||||||
|
onImportDays,
|
||||||
|
}) => {
|
||||||
|
// Export/Import options
|
||||||
|
const [exportOptions, setExportOptions] = useState<ExportImportOptions>({
|
||||||
|
includeSchedules: true,
|
||||||
|
includeDiagramSettings: true,
|
||||||
|
includeSimulationSettings: true,
|
||||||
|
includePharmacoSettings: true,
|
||||||
|
includeAdvancedSettings: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [importOptions, setImportOptions] = useState<ExportImportOptions>({
|
||||||
|
includeSchedules: true,
|
||||||
|
includeDiagramSettings: true,
|
||||||
|
includeSimulationSettings: true,
|
||||||
|
includePharmacoSettings: true,
|
||||||
|
includeAdvancedSettings: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// File upload state
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// JSON editor state
|
||||||
|
const [jsonEditorExpanded, setJsonEditorExpanded] = useState(false);
|
||||||
|
const [jsonEditorContent, setJsonEditorContent] = useState('');
|
||||||
|
const [jsonValidationMessage, setJsonValidationMessage] = useState<{
|
||||||
|
type: 'success' | 'error' | null;
|
||||||
|
message: string;
|
||||||
|
}>({ type: null, message: '' });
|
||||||
|
|
||||||
|
// Clipboard feedback
|
||||||
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
|
|
||||||
|
// Reset editor when modal opens/closes
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
// Load current app data into editor when opening
|
||||||
|
const appState = {
|
||||||
|
pkParams,
|
||||||
|
days,
|
||||||
|
therapeuticRange,
|
||||||
|
doseIncrement,
|
||||||
|
uiSettings,
|
||||||
|
steadyStateConfig: { daysOnMedication: pkParams.advanced.steadyStateDays },
|
||||||
|
};
|
||||||
|
const exportData = exportSettings(appState, exportOptions, APP_VERSION);
|
||||||
|
const jsonString = JSON.stringify(exportData, null, 2);
|
||||||
|
setJsonEditorContent(jsonString);
|
||||||
|
setJsonEditorExpanded(true);
|
||||||
|
validateJsonContent(jsonString);
|
||||||
|
} else {
|
||||||
|
// Clear editor when closing
|
||||||
|
setJsonEditorContent('');
|
||||||
|
setJsonEditorExpanded(false);
|
||||||
|
setJsonValidationMessage({ type: null, message: '' });
|
||||||
|
setSelectedFile(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
// Handle export to file
|
||||||
|
const handleExportToFile = () => {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle copy to clipboard
|
||||||
|
const handleCopyToClipboard = async () => {
|
||||||
|
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);
|
||||||
|
const jsonString = JSON.stringify(exportData, null, 2);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try modern Clipboard API first
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
await navigator.clipboard.writeText(jsonString);
|
||||||
|
setCopySuccess(true);
|
||||||
|
setJsonEditorContent(jsonString);
|
||||||
|
setJsonEditorExpanded(true);
|
||||||
|
validateJsonContent(jsonString);
|
||||||
|
setTimeout(() => setCopySuccess(false), 2000);
|
||||||
|
} else {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = jsonString;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-999999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
setCopySuccess(true);
|
||||||
|
setJsonEditorContent(jsonString);
|
||||||
|
setJsonEditorExpanded(true);
|
||||||
|
validateJsonContent(jsonString);
|
||||||
|
setTimeout(() => setCopySuccess(false), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fallback copy failed:', err);
|
||||||
|
alert(t('copyFailed'));
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Copy to clipboard failed:', error);
|
||||||
|
alert(t('copyFailed'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle paste from clipboard
|
||||||
|
const handlePasteFromClipboard = async () => {
|
||||||
|
try {
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
// Try modern Clipboard API first
|
||||||
|
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||||
|
text = await navigator.clipboard.readText();
|
||||||
|
} else {
|
||||||
|
// Fallback: show message and open editor for manual paste
|
||||||
|
alert(t('pasteNoClipboardApi'));
|
||||||
|
setJsonEditorExpanded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate content size (max 5000 characters)
|
||||||
|
if (text.length > 5000) {
|
||||||
|
alert(t('pasteContentTooLarge'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update editor and validate
|
||||||
|
setJsonEditorContent(text);
|
||||||
|
setJsonEditorExpanded(true);
|
||||||
|
validateJsonContent(text);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Paste from clipboard failed:', error);
|
||||||
|
alert(t('pasteFailed'));
|
||||||
|
// Still show editor for manual paste
|
||||||
|
setJsonEditorExpanded(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle file selection
|
||||||
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
setSelectedFile(file || null);
|
||||||
|
if (file) {
|
||||||
|
// Automatically read and show in JSON editor
|
||||||
|
file.text().then(content => {
|
||||||
|
setJsonEditorContent(content);
|
||||||
|
setJsonEditorExpanded(true);
|
||||||
|
validateJsonContent(content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate JSON content
|
||||||
|
const validateJsonContent = (content: string) => {
|
||||||
|
if (!content.trim()) {
|
||||||
|
setJsonValidationMessage({ type: null, message: '' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
const importData = parseImportFile(content);
|
||||||
|
|
||||||
|
if (!importData) {
|
||||||
|
setJsonValidationMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: t('importParseError'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = validateImportData(importData);
|
||||||
|
|
||||||
|
if (!validation.isValid) {
|
||||||
|
setJsonValidationMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: validation.errors.join(', '),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validation.warnings.length > 0) {
|
||||||
|
setJsonValidationMessage({
|
||||||
|
type: 'success',
|
||||||
|
message: t('jsonValidationSuccess') + ' ⚠️ ' + validation.warnings.length + ' warnings',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setJsonValidationMessage({
|
||||||
|
type: 'success',
|
||||||
|
message: t('jsonValidationSuccess'),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setJsonValidationMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: t('pasteInvalidJson'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle JSON editor content change
|
||||||
|
const handleJsonEditorChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const content = e.target.value;
|
||||||
|
setJsonEditorContent(content);
|
||||||
|
validateJsonContent(content);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle import from JSON editor or file
|
||||||
|
const handleImport = async () => {
|
||||||
|
const hasAnySelected = Object.values(importOptions).some(v => v);
|
||||||
|
if (!hasAnySelected) {
|
||||||
|
alert(t('importNoOptionsSelected'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileContent = jsonEditorContent;
|
||||||
|
|
||||||
|
// If no JSON in editor but file selected, read file
|
||||||
|
if (!fileContent && selectedFile) {
|
||||||
|
fileContent = await selectedFile.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileContent) {
|
||||||
|
alert(t('importFileNotSelected'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
setJsonEditorContent('');
|
||||||
|
setJsonValidationMessage({ type: null, message: '' });
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Import error:', error);
|
||||||
|
alert(t('importError'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear JSON editor
|
||||||
|
const handleClearJson = () => {
|
||||||
|
setJsonEditorContent('');
|
||||||
|
setJsonValidationMessage({ type: null, message: '' });
|
||||||
|
setSelectedFile(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||||
|
<div className="max-w-4xl w-full max-h-[90vh] overflow-y-auto bg-background rounded-lg shadow-xl">
|
||||||
|
<Card className="border-0">
|
||||||
|
<CardHeader className="bg-primary/10 border-b">
|
||||||
|
<CardTitle className="text-2xl font-bold">
|
||||||
|
{t('dataManagementTitle')}
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{t('dataManagementSubtitle')}
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6 pt-6">
|
||||||
|
{/* Export Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileJson className="h-5 w-5 text-primary" />
|
||||||
|
<h3 className="text-lg font-semibold">{t('exportSettings')}</h3>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="text-xs max-w-xs">{t('exportImportTooltip')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium">{t('exportSelectWhat')}</Label>
|
||||||
|
<div className="space-y-2 pl-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<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-3">
|
||||||
|
<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-3">
|
||||||
|
<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-3">
|
||||||
|
<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-3">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Export Actions - Mobile-friendly button group */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleExportToFile}
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
{t('exportButton')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCopyToClipboard}
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
{copySuccess ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
{t('copiedToClipboard')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
{t('copyToClipboard')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Import Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileJson className="h-5 w-5 text-primary" />
|
||||||
|
<h3 className="text-lg font-semibold">{t('importSettings')}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium">{t('importSelectWhat')}</Label>
|
||||||
|
<div className="space-y-2 pl-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<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-3">
|
||||||
|
<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-3">
|
||||||
|
<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-3">
|
||||||
|
<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-3">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Import Actions - Mobile-friendly button group */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4" />
|
||||||
|
{t('importButton')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handlePasteFromClipboard}
|
||||||
|
className="flex-1 gap-2"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<ClipboardPaste className="h-4 w-4" />
|
||||||
|
{t('pasteFromClipboard')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hidden file input */}
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedFile && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{t('importFileSelected')} <span className="font-medium">{selectedFile.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* JSON Editor Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => setJsonEditorExpanded(!jsonEditorExpanded)}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-between"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<FileJson className="h-4 w-4" />
|
||||||
|
{jsonEditorExpanded ? t('hideJsonEditor') : t('showJsonEditor')}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={`h-4 w-4 transition-transform ${
|
||||||
|
jsonEditorExpanded ? 'rotate-180' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{jsonEditorExpanded && (
|
||||||
|
<div className="space-y-3 animate-in fade-in slide-in-from-top-2 duration-200">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="json-editor" className="text-sm font-medium">
|
||||||
|
{t('jsonEditorLabel')}
|
||||||
|
</Label>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="text-xs max-w-xs">{t('jsonEditorTooltip')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
id="json-editor"
|
||||||
|
value={jsonEditorContent}
|
||||||
|
onChange={handleJsonEditorChange}
|
||||||
|
placeholder={t('jsonEditorPlaceholder')}
|
||||||
|
className="font-mono text-xs min-h-[200px] max-h-[400px]"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{jsonValidationMessage.type && (
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-2 text-sm ${
|
||||||
|
jsonValidationMessage.type === 'success'
|
||||||
|
? 'text-green-600 dark:text-green-400'
|
||||||
|
: 'text-red-600 dark:text-red-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{jsonValidationMessage.type === 'success' ? (
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span>{jsonValidationMessage.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => validateJsonContent(jsonEditorContent)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{t('validateJson')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleClearJson}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{t('clearJson')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleImport}
|
||||||
|
className="flex-1"
|
||||||
|
size="lg"
|
||||||
|
disabled={!jsonEditorContent && !selectedFile}
|
||||||
|
>
|
||||||
|
{t('importApplyButton')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose} variant="outline" className="flex-1" size="lg">
|
||||||
|
{t('closeDataManagement')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataManagementModal;
|
||||||
@@ -20,8 +20,7 @@ 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, APP_VERSION } from '../constants/defaults';
|
import { getDefaultState } 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.
|
||||||
@@ -120,6 +119,7 @@ const Settings = ({
|
|||||||
onUpdateUiSetting,
|
onUpdateUiSetting,
|
||||||
onReset,
|
onReset,
|
||||||
onImportDays,
|
onImportDays,
|
||||||
|
onOpenDataManagement,
|
||||||
t
|
t
|
||||||
}: any) => {
|
}: any) => {
|
||||||
const { showDayTimeOnXAxis, yAxisMin, yAxisMax, showTemplateDay, simulationDays, displayedDays } = uiSettings;
|
const { showDayTimeOnXAxis, yAxisMin, yAxisMax, showTemplateDay, simulationDays, displayedDays } = uiSettings;
|
||||||
@@ -131,24 +131,6 @@ 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);
|
||||||
@@ -181,7 +163,6 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -221,27 +202,22 @@ const Settings = ({
|
|||||||
|
|
||||||
const updateDiagramExpanded = (value: boolean) => {
|
const updateDiagramExpanded = (value: boolean) => {
|
||||||
setIsDiagramExpanded(value);
|
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) => {
|
const updateSimulationExpanded = (value: boolean) => {
|
||||||
setIsSimulationExpanded(value);
|
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) => {
|
const updatePharmacokineticExpanded = (value: boolean) => {
|
||||||
setIsPharmacokineticExpanded(value);
|
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) => {
|
const updateAdvancedExpanded = (value: boolean) => {
|
||||||
setIsAdvancedExpanded(value);
|
setIsAdvancedExpanded(value);
|
||||||
saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: value, dataManagement: isDataManagementExpanded });
|
saveCardStates({ diagram: isDiagramExpanded, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: value });
|
||||||
};
|
|
||||||
|
|
||||||
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) => {
|
||||||
@@ -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
|
// 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);
|
||||||
@@ -1254,187 +1121,15 @@ const Settings = ({
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Export/Import Settings Card */}
|
{/* Data Management Button */}
|
||||||
<Card>
|
<Button
|
||||||
<CollapsibleCardHeader
|
type="button"
|
||||||
title={t('dataManagement')}
|
onClick={onOpenDataManagement}
|
||||||
isCollapsed={!isDataManagementExpanded}
|
variant="outline"
|
||||||
onToggle={() => updateDataManagementExpanded(!isDataManagementExpanded)}
|
className="w-full"
|
||||||
/>
|
>
|
||||||
{isDataManagementExpanded && (
|
{t('openDataManagement')}
|
||||||
<CardContent className="space-y-4">
|
</Button>
|
||||||
<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
|
||||||
|
|||||||
21
src/components/ui/textarea.tsx
Normal file
21
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -218,6 +218,33 @@ export const de = {
|
|||||||
importFileNotSelected: "Keine 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.",
|
exportImportTooltip: "Exportiere deine Einstellungen als Backup oder zum Teilen. Importiere zuvor exportierte Einstellungen. Wähle individuell, welche Teile exportiert/importiert werden sollen.",
|
||||||
|
|
||||||
|
// Data Management Modal
|
||||||
|
dataManagementTitle: "Datenverwaltung",
|
||||||
|
dataManagementSubtitle: "Exportieren, importieren und verwalten Sie Ihre Anwendungsdaten",
|
||||||
|
openDataManagement: "Daten verwalten...",
|
||||||
|
copyToClipboard: "In Zwischenablage kopieren",
|
||||||
|
pasteFromClipboard: "Aus Zwischenablage einfügen",
|
||||||
|
exportActions: "Export-Aktionen",
|
||||||
|
importActions: "Import-Aktionen",
|
||||||
|
showJsonEditor: "JSON-Editor anzeigen",
|
||||||
|
hideJsonEditor: "JSON-Editor ausblenden",
|
||||||
|
jsonEditorLabel: "JSON-Editor",
|
||||||
|
jsonEditorPlaceholder: "Fügen Sie hier Ihr JSON-Backup ein oder bearbeiten Sie die exportierten Daten...",
|
||||||
|
jsonEditorTooltip: "Bearbeiten Sie exportierte Daten direkt oder fügen Sie Backup-JSON ein. Manuelle Bearbeitung erfordert JSON-Kenntnisse.",
|
||||||
|
copiedToClipboard: "In Zwischenablage kopiert!",
|
||||||
|
copyFailed: "Kopieren in Zwischenablage fehlgeschlagen",
|
||||||
|
pasteSuccess: "JSON erfolgreich eingefügt",
|
||||||
|
pasteFailed: "Einfügen aus Zwischenablage fehlgeschlagen",
|
||||||
|
pasteNoClipboardApi: "Zwischenablage-Zugriff nicht verfügbar. Bitte manuell einfügen.",
|
||||||
|
pasteInvalidJson: "Ungültiges JSON-Format. Bitte überprüfen Sie Ihre Daten.",
|
||||||
|
jsonEditWarning: "⚠️ Manuelle Bearbeitung erfordert JSON-Kenntnisse. Ungültige Daten können Fehler verursachen.",
|
||||||
|
validateJson: "JSON validieren",
|
||||||
|
clearJson: "Löschen",
|
||||||
|
jsonValidationSuccess: "JSON ist gültig",
|
||||||
|
jsonValidationError: "✗ Ungültiges JSON",
|
||||||
|
closeDataManagement: "Schließen",
|
||||||
|
pasteContentTooLarge: "Inhalt zu groß (max. 5000 Zeichen)",
|
||||||
|
|
||||||
// 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.",
|
||||||
|
|||||||
@@ -216,6 +216,33 @@ export const en = {
|
|||||||
importFileNotSelected: "No 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.",
|
exportImportTooltip: "Export your settings as backup or share with others. Import previously exported settings. Choose which parts to export/import individually.",
|
||||||
|
|
||||||
|
// Data Management Modal
|
||||||
|
dataManagementTitle: "Data Management",
|
||||||
|
dataManagementSubtitle: "Export, import, and manage your application data",
|
||||||
|
openDataManagement: "Manage Data...",
|
||||||
|
copyToClipboard: "Copy to Clipboard",
|
||||||
|
pasteFromClipboard: "Paste from Clipboard",
|
||||||
|
exportActions: "Export Actions",
|
||||||
|
importActions: "Import Actions",
|
||||||
|
showJsonEditor: "Show JSON Editor",
|
||||||
|
hideJsonEditor: "Hide JSON Editor",
|
||||||
|
jsonEditorLabel: "JSON Editor",
|
||||||
|
jsonEditorPlaceholder: "Paste your JSON backup here or edit the exported data...",
|
||||||
|
jsonEditorTooltip: "Edit exported data directly or paste backup JSON. Manual editing requires JSON knowledge.",
|
||||||
|
copiedToClipboard: "Copied to clipboard!",
|
||||||
|
copyFailed: "Failed to copy to clipboard",
|
||||||
|
pasteSuccess: "JSON pasted successfully",
|
||||||
|
pasteFailed: "Failed to paste from clipboard",
|
||||||
|
pasteNoClipboardApi: "Clipboard access not available. Please paste manually.",
|
||||||
|
pasteInvalidJson: "Invalid JSON format. Please check your data.",
|
||||||
|
jsonEditWarning: "⚠️ Manual editing requires JSON knowledge. Invalid data may cause errors.",
|
||||||
|
validateJson: "Validate JSON",
|
||||||
|
clearJson: "Clear",
|
||||||
|
jsonValidationSuccess: "JSON is valid",
|
||||||
|
jsonValidationError: "✗ Invalid JSON",
|
||||||
|
closeDataManagement: "Close",
|
||||||
|
pasteContentTooLarge: "Content too large (max. 5000 characters)",
|
||||||
|
|
||||||
// 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.",
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export const validateImportData = (data: any): ImportValidationResult => {
|
|||||||
|
|
||||||
// Validate advanced settings
|
// Validate advanced settings
|
||||||
if (importData.advancedSettings !== undefined) {
|
if (importData.advancedSettings !== undefined) {
|
||||||
const validCategories = ['weightBasedVd', 'foodEffect', 'urinePh', 'fOral', 'steadyStateDays'];
|
const validCategories = ['standardVd', 'weightBasedVd', 'foodEffect', 'urinePh', 'fOral', 'steadyStateDays'];
|
||||||
const importedCategories = Object.keys(importData.advancedSettings);
|
const importedCategories = Object.keys(importData.advancedSettings);
|
||||||
const unknownCategories = importedCategories.filter(c => !validCategories.includes(c));
|
const unknownCategories = importedCategories.filter(c => !validCategories.includes(c));
|
||||||
if (unknownCategories.length > 0) {
|
if (unknownCategories.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user