Compare commits
1 Commits
main
...
e7d64fb8ab
| Author | SHA1 | Date | |
|---|---|---|---|
| e7d64fb8ab |
29
docs/TODO.md
Normal file
29
docs/TODO.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
- F_ORAL: i would say for now we add all the values we have, and that are not e.g. natural physical constants that noone would ever want to change (vs. everything that could deviate from person to person and that can make a visible / measurable difference), to the (in doubt "advanced" section of the) ui for transperency and flexibility reasons. with reasonable (not to restrictive but in the realm of possibility) min/max values. in this case one could argue that everyone should take those meds oraly, but if we make it transparent everyone can see that this might e.g. be the reason that our simulation slightly deviates from some statistics or other simulations (they may not be aware of this parameter and they might want to know more about it). also, in some rare cases people may take meds via other routes (e.g. in hospital settings), so having this parameter editable could be useful in some edge cases.
|
||||||
|
|
||||||
|
- an additional topic that came to mind (unless already mentioned earlier): we always simulate a fixed number of days to get a "stable" simulation. the user should be allowed to set this number of days as a parameter. this way one can e.g. simulate their first day starting from scratch without assuming any prior history.
|
||||||
|
|
||||||
|
- an additional topic that came to mind (unless already mentioned earlier): we always simulate a fixed number of days to get a "stable" simulation. the user should be allowed to set this number of days as a parameter. this way one can e.g. simulate their first day starting from scratch without assuming any prior history.
|
||||||
|
|
||||||
|
- regarding half-life vs. factor at first glance i would assume that users may be more familiar with the prinzipal of the half-life, so we make the half-life(s) editable in the ui and use the factors in our formular (if needed). i doubt it wouldn't hurt if we calculate and show the factors (where applicable) in the ui, either next to the half-life (h) input field(s) or, in case it generally makes sense to show a small (see "summary" below).
|
||||||
|
|
||||||
|
- therapeutic range and some other defaults: this is currently my own range (most likely a bit lower and a bit wider but not by much), which may apply for other values as well (e.g. the absorption hal-life of 1.5 vs. 0.9 you mentioned, it may be that the ai suggested this initially, or i found this value somewhere, or in doubt is imply changed to what "looked how i feel" when i follow the simulated plan. so in doubt, always choose the more "general" and appropriate values as defaults and i can later change them to my own liking/needs in my personal profile
|
||||||
|
- reason: this is my own project/prototype and i am still tweaking. at least for now it dint's share it widely, just with 1-2 friends and family but if it seems helpful i might post it in a forum with my experiences (why hide it if it could potentially help others to understand the basic mechanisms).
|
||||||
|
- age selection?: it seems that this (and maybe other values as well) not only varies between persons, it also seems to be quite different between adults (lower) and children (higher), or in general depending on age. at first glance a more fine grained age range seems overkill, but we could provide a selection for Adult vs. Child (6-12) or similar at least if we can make this work in a reasonably valid way. otherwise we can stick with adults in general users could still manually change some settings to make it more/less work for children too (e.g. reduce the volume of distribution Vd value or the values that influence Vd). if this does not seem viable or too complex, then for simplicity reasons we could just start with allowing the "full range" (applicable for adults and children) and the users can make some research on their own and tweak the relevant values to their needs in an attempt e.g. to simulate for children. according to the ai research, for children taking 70mg LDX the Men C max might be around 134.0 ng/mL (vs. 69 to 80.3 ng/mL for adults taking 70mg LDX), where supra-therapeutic / alert levels might start above 100 ng/mL for adults (unknown for children from the data i have, but presumably above the 134.0 ng/mL).
|
||||||
|
|
||||||
|
- Advanced (collapsed):
|
||||||
|
- body weight based scaling: on/off toggle (default off) with a kg input field (enabled and used only if toggle=on).
|
||||||
|
- "Taken with Food (High-Fat Meal)?": on/off toggle (default off) with a tmax input field (enabled and used only if toggle=on, default value: +1h). either a regular +/- button decimal input field (one decimal digit should suffice i guess?). min max could be 0 to 2h maybe? (where 0 is the same as disabling the feature).
|
||||||
|
- consider urin ph tendency?: on/off toggle (default off), again with a decimal input field
|
||||||
|
|
||||||
|
- regarding disclaimer etc.: when it hink about it, since i'm based in DE (i.e. EU) we should focus on that and not US (unless necessary). i don't have abusiness and i am not selling anything, the app is hosted open source, so i am not even sure if we need anything / much. i just want to make make sure that this is just a hobbyist project without any guaranties or liabilities.
|
||||||
|
|
||||||
|
- i would not start to use sliders for now. the +/- input fields should provide sufficient control on mobile (+/- buttons) and desktop (input field). if need be i can change this later.
|
||||||
|
|
||||||
|
- summary: to make things more transparent we could e.g. show a table with the individual values, or a step by step description of the individual variables used in the simulation formula(s) with interim caclualations and the final formal (surely at least the dosage and time will always stay variables). something like this might not be overly complicated (e.g. simple table for the former, or in text form maybe similar to the step by step description in the AppPharmacokineticsImprovementPlan.md "5. Computational Modeling and Simulation Parameters" (formula, individual parameters with short description and actual values based on the user input). this could be a foldable box on the left side under the plan section or if need be a full width (two columns) box above the final desclaimer/footer box. if this sounds to complex or needs more discussion / input (to make at least a prototype that i can evaluate), we can leave this for now and maybe add it later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
make it so that the "Medication History" simulation setting uses a toggle so that the user can quickly toggle between no medication history (starting with the first day) and if on, the medication history days field shows (we can still allow min = 0 even if it's simulation wise the same as deactivating the feature, but we have a similar behavior in other placeses e.g. Taken With Meal / Absorptoin Dealy).
|
||||||
|
|
||||||
|
|
||||||
|
let's allow a simulation duration of 1 and call it something like "Minimum simulation duration" so users can set it to 1 to see only the chart with a single day without scrollbars. i already tried to set min=1 but there are some problems (we set it to 3 for a reason at the time). there are at least some placeses where this value is used, e.g. the calculate/generate the x-axis ticks (at least if i set it to 1 but have two alternate plans added, meaning there are 3 days in the simulation which i see in the curve but day 2 and 3 have x-axis ticks. please ensure that everywher where te value is used as is, it takes this values e.g. as max(minSimulationDays, ) (where minSimulationDays means the current field's alue).
|
||||||
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