Update data deletion now in data manager with customization, minor UI improvements, increased chart y-axis tick count (regression)

This commit is contained in:
2026-02-04 12:24:03 +00:00
parent 11dacb5441
commit efa45ab288
10 changed files with 469 additions and 48 deletions

View File

@@ -24,6 +24,8 @@ 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';
import { PROJECT_REPOSITORY_URL, APP_VERSION } from './constants/defaults'; import { PROJECT_REPOSITORY_URL, APP_VERSION } from './constants/defaults';
import { deleteSelectedData } from './utils/exportImport';
import type { ExportOptions } from './utils/exportImport';
// Custom Hooks // Custom Hooks
import { useAppState } from './hooks/useAppState'; import { useAppState } from './hooks/useAppState';
@@ -74,7 +76,6 @@ const MedPlanAssistant = () => {
updateState, updateState,
updateNestedState, updateNestedState,
updateUiSetting, updateUiSetting,
handleReset,
addDay, addDay,
removeDay, removeDay,
addDoseToDay, addDoseToDay,
@@ -135,6 +136,29 @@ const MedPlanAssistant = () => {
templateProfile templateProfile
} = useSimulation(appState); } = useSimulation(appState);
// Handle data deletion
const handleDeleteData = (options: ExportOptions) => {
const newState = deleteSelectedData(appState, options);
// Apply all state updates
Object.entries(newState).forEach(([key, value]) => {
if (key === 'days') {
updateState('days', value as any);
} else if (key === 'pkParams') {
updateState('pkParams', value as any);
} else if (key === 'therapeuticRange') {
updateState('therapeuticRange', value as any);
} else if (key === 'doseIncrement') {
updateState('doseIncrement', value as any);
} else if (key === 'uiSettings') {
// Update UI settings individually
Object.entries(value as any).forEach(([uiKey, uiValue]) => {
updateUiSetting(uiKey as any, uiValue);
});
}
});
};
return ( return (
<TooltipProvider> <TooltipProvider>
<div className="min-h-screen bg-background p-4 sm:p-6 lg:p-8"> <div className="min-h-screen bg-background p-4 sm:p-6 lg:p-8">
@@ -163,6 +187,7 @@ const MedPlanAssistant = () => {
onUpdateTherapeuticRange={(key: any, value: any) => updateNestedState('therapeuticRange', key, value)} onUpdateTherapeuticRange={(key: any, value: any) => updateNestedState('therapeuticRange', key, value)}
onUpdateUiSetting={(key: any, value: any) => updateUiSetting(key as any, value)} onUpdateUiSetting={(key: any, value: any) => updateUiSetting(key as any, value)}
onImportDays={(importedDays: any) => updateState('days', importedDays)} onImportDays={(importedDays: any) => updateState('days', importedDays)}
onDeleteData={handleDeleteData}
/> />
<div className="max-w-7xl mx-auto" style={{ <div className="max-w-7xl mx-auto" style={{
@@ -287,7 +312,6 @@ const MedPlanAssistant = () => {
onUpdatePkParams={(key: any, value: any) => updateNestedState('pkParams', key, value)} onUpdatePkParams={(key: any, value: any) => updateNestedState('pkParams', key, value)}
onUpdateTherapeuticRange={(key: any, value: any) => updateNestedState('therapeuticRange', key, value)} onUpdateTherapeuticRange={(key: any, value: any) => updateNestedState('therapeuticRange', key, value)}
onUpdateUiSetting={updateUiSetting} onUpdateUiSetting={updateUiSetting}
onReset={handleReset}
onImportDays={(importedDays: any) => updateState('days', importedDays)} onImportDays={(importedDays: any) => updateState('days', importedDays)}
onOpenDataManagement={() => setShowDataManagement(true)} onOpenDataManagement={() => setShowDataManagement(true)}
t={t} t={t}

View File

@@ -21,6 +21,7 @@ import { Switch } from './ui/switch';
import { Separator } from './ui/separator'; import { Separator } from './ui/separator';
import { Textarea } from './ui/textarea'; import { Textarea } from './ui/textarea';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { Trash2 } from 'lucide-react';
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@@ -52,6 +53,7 @@ interface ExportImportOptions {
includeSimulationSettings: boolean; includeSimulationSettings: boolean;
includePharmacoSettings: boolean; includePharmacoSettings: boolean;
includeAdvancedSettings: boolean; includeAdvancedSettings: boolean;
includeOtherData: boolean;
} }
interface DataManagementModalProps { interface DataManagementModalProps {
@@ -69,6 +71,7 @@ interface DataManagementModalProps {
onUpdateTherapeuticRange: (key: string, value: any) => void; onUpdateTherapeuticRange: (key: string, value: any) => void;
onUpdateUiSetting: (key: string, value: any) => void; onUpdateUiSetting: (key: string, value: any) => void;
onImportDays?: (days: any) => void; onImportDays?: (days: any) => void;
onDeleteData?: (options: ExportImportOptions) => void;
} }
const DataManagementModal: React.FC<DataManagementModalProps> = ({ const DataManagementModal: React.FC<DataManagementModalProps> = ({
@@ -84,6 +87,7 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
onUpdateTherapeuticRange, onUpdateTherapeuticRange,
onUpdateUiSetting, onUpdateUiSetting,
onImportDays, onImportDays,
onDeleteData,
}) => { }) => {
// Export/Import options // Export/Import options
const [exportOptions, setExportOptions] = useState<ExportImportOptions>({ const [exportOptions, setExportOptions] = useState<ExportImportOptions>({
@@ -92,6 +96,7 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
includeSimulationSettings: true, includeSimulationSettings: true,
includePharmacoSettings: true, includePharmacoSettings: true,
includeAdvancedSettings: true, includeAdvancedSettings: true,
includeOtherData: false,
}); });
const [importOptions, setImportOptions] = useState<ExportImportOptions>({ const [importOptions, setImportOptions] = useState<ExportImportOptions>({
@@ -100,6 +105,17 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
includeSimulationSettings: true, includeSimulationSettings: true,
includePharmacoSettings: true, includePharmacoSettings: true,
includeAdvancedSettings: true, includeAdvancedSettings: true,
includeOtherData: false,
});
// Deletion options - defaults: all except otherData
const [deletionOptions, setDeletionOptions] = useState<ExportImportOptions>({
includeSchedules: true,
includeDiagramSettings: true,
includeSimulationSettings: true,
includePharmacoSettings: true,
includeAdvancedSettings: true,
includeOtherData: true,
}); });
// File upload state // File upload state
@@ -445,6 +461,40 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
} }
}; };
// Handle delete selected data
const handleDeleteData = () => {
const hasAnySelected = Object.values(deletionOptions).some(v => v);
if (!hasAnySelected) {
alert(t('deleteNoOptionsSelected'));
return;
}
// Build confirmation message listing what will be deleted
const categoriesToDelete = [];
if (deletionOptions.includeSchedules) categoriesToDelete.push(t('exportOptionSchedules'));
if (deletionOptions.includeDiagramSettings) categoriesToDelete.push(t('exportOptionDiagram'));
if (deletionOptions.includeSimulationSettings) categoriesToDelete.push(t('exportOptionSimulation'));
if (deletionOptions.includePharmacoSettings) categoriesToDelete.push(t('exportOptionPharmaco'));
if (deletionOptions.includeAdvancedSettings) categoriesToDelete.push(t('exportOptionAdvanced'));
if (deletionOptions.includeOtherData) categoriesToDelete.push(t('exportOptionOtherData'));
const confirmMessage =
t('deleteDataConfirmTitle') + '\n\n' +
categoriesToDelete.map(cat => `${cat}`).join('\n') + '\n\n' +
t('deleteDataConfirmWarning');
if (!window.confirm(confirmMessage)) {
return;
}
// Call deletion handler
if (onDeleteData) {
onDeleteData(deletionOptions);
alert(t('deleteDataSuccess'));
onClose();
}
};
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> <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"> <div className="max-w-4xl w-full max-h-[90vh] overflow-y-auto bg-background rounded-lg shadow-xl">
@@ -542,6 +592,31 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
{t('exportOptionAdvanced')} {t('exportOptionAdvanced')}
</Label> </Label>
</div> </div>
<div className="flex items-center gap-3">
<Switch
id="export-other"
checked={exportOptions.includeOtherData}
onCheckedChange={checked =>
setExportOptions({ ...exportOptions, includeOtherData: checked })
}
/>
<Label htmlFor="export-other" className="text-sm">
{t('exportOptionOtherData')}
</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('exportOptionOtherDataTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
</div> </div>
</div> </div>
@@ -647,6 +722,31 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
{t('exportOptionAdvanced')} {t('exportOptionAdvanced')}
</Label> </Label>
</div> </div>
<div className="flex items-center gap-3">
<Switch
id="import-other"
checked={importOptions.includeOtherData}
onCheckedChange={checked =>
setImportOptions({ ...importOptions, includeOtherData: checked })
}
/>
<Label htmlFor="import-other" className="text-sm">
{t('exportOptionOtherData')}
</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('exportOptionOtherDataTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
</div> </div>
</div> </div>
@@ -791,22 +891,138 @@ const DataManagementModal: React.FC<DataManagementModalProps> = ({
)} )}
</div> </div>
<Separator /> {/* Apply Import Button - directly below JSON editor without separator */}
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3">
<Button <Button
onClick={handleImport} onClick={handleImport}
className="flex-1" className="w-full"
size="lg" size="lg"
disabled={!jsonEditorContent && !selectedFile} disabled={!jsonEditorContent && !selectedFile}
> >
{t('importApplyButton')} {t('importApplyButton')}
</Button> </Button>
<Button onClick={onClose} variant="outline" className="flex-1" size="lg">
{t('closeDataManagement')} <Separator />
{/* Delete Specific Data Section */}
<div className="space-y-4">
<div className="flex i-tems-center gap-2">
<div className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-destructive" />
</div>
<h3 className="text-lg font-semibold text-destructive">{t('deleteSpecificData')}</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('deleteSpecificDataTooltip')}</p>
</TooltipContent>
</Tooltip>
</div>
{/* Warning Message */}
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3 text-sm">
<p className="text-yellow-800 dark:text-yellow-200">{t('deleteDataWarning')}</p>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">{t('deleteSelectWhat')}</Label>
<div className="space-y-2 pl-4 [&_button[role=switch][data-state=checked]]:bg-destructive [&_button[role=switch][data-state=checked]]:border-destructive">
<div className="flex items-center gap-3">
<Switch
id="delete-schedules"
checked={deletionOptions.includeSchedules}
onCheckedChange={checked =>
setDeletionOptions({ ...deletionOptions, includeSchedules: checked })
}
/>
<Label htmlFor="delete-schedules" className="text-sm">
{t('exportOptionSchedules')}
</Label>
</div>
<div className="flex items-center gap-3">
<Switch
id="delete-diagram"
checked={deletionOptions.includeDiagramSettings}
onCheckedChange={checked =>
setDeletionOptions({ ...deletionOptions, includeDiagramSettings: checked })
}
/>
<Label htmlFor="delete-diagram" className="text-sm">
{t('exportOptionDiagram')}
</Label>
</div>
<div className="flex items-center gap-3">
<Switch
id="delete-simulation"
checked={deletionOptions.includeSimulationSettings}
onCheckedChange={checked =>
setDeletionOptions({ ...deletionOptions, includeSimulationSettings: checked })
}
/>
<Label htmlFor="delete-simulation" className="text-sm">
{t('exportOptionSimulation')}
</Label>
</div>
<div className="flex items-center gap-3">
<Switch
id="delete-pharmaco"
checked={deletionOptions.includePharmacoSettings}
onCheckedChange={checked =>
setDeletionOptions({ ...deletionOptions, includePharmacoSettings: checked })
}
/>
<Label htmlFor="delete-pharmaco" className="text-sm">
{t('exportOptionPharmaco')}
</Label>
</div>
<div className="flex items-center gap-3">
<Switch
id="delete-advanced"
checked={deletionOptions.includeAdvancedSettings}
onCheckedChange={checked =>
setDeletionOptions({ ...deletionOptions, includeAdvancedSettings: checked })
}
/>
<Label htmlFor="delete-advanced" className="text-sm">
{t('exportOptionAdvanced')}
</Label>
</div>
<div className="flex items-center gap-3">
<Switch
id="delete-other"
checked={deletionOptions.includeOtherData}
onCheckedChange={checked =>
setDeletionOptions({ ...deletionOptions, includeOtherData: checked })
}
/>
<Label htmlFor="delete-other" className="text-sm">
{t('exportOptionOtherData')}
</Label>
</div>
</div>
</div>
{/* Delete Button */}
<Button
onClick={handleDeleteData}
variant="destructive"
className="w-full"
size="lg"
>
{t('deleteDataButton')}
</Button> </Button>
</div> </div>
{/* Close Button */}
<Button onClick={onClose} variant="outline" className="w-full" size="lg">
{t('closeDataManagement')}
</Button>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@@ -116,7 +116,6 @@ const Settings = ({
onUpdatePkParams, onUpdatePkParams,
onUpdateTherapeuticRange, onUpdateTherapeuticRange,
onUpdateUiSetting, onUpdateUiSetting,
onReset,
onImportDays, onImportDays,
onOpenDataManagement, onOpenDataManagement,
t t
@@ -458,6 +457,8 @@ const Settings = ({
unit={t('unitDays')} unit={t('unitDays')}
required={true} required={true}
errorMessage={t('errorNumberRequired')} errorMessage={t('errorNumberRequired')}
showResetButton={true}
defaultValue="2"
/> />
</div> </div>
@@ -490,7 +491,8 @@ const Settings = ({
max={500} max={500}
placeholder={t('auto')} placeholder={t('auto')}
allowEmpty={true} allowEmpty={true}
clearButton={true} showResetButton={true}
defaultValue=""
warning={!!yAxisRangeError} warning={!!yAxisRangeError}
warningMessage={yAxisRangeError} warningMessage={yAxisRangeError}
/> />
@@ -504,7 +506,8 @@ const Settings = ({
placeholder={t('auto')} placeholder={t('auto')}
unit="ng/ml" unit="ng/ml"
allowEmpty={true} allowEmpty={true}
clearButton={true} showResetButton={true}
defaultValue=""
warning={!!yAxisRangeError} warning={!!yAxisRangeError}
warningMessage={yAxisRangeError} warningMessage={yAxisRangeError}
/> />
@@ -598,6 +601,8 @@ const Settings = ({
unit={t('unitDays')} unit={t('unitDays')}
required={true} required={true}
errorMessage={t('errorNumberRequired')} errorMessage={t('errorNumberRequired')}
showResetButton={true}
defaultValue="5"
/> />
</div> </div>
@@ -649,6 +654,8 @@ const Settings = ({
max={7} max={7}
unit={t('unitDays')} unit={t('unitDays')}
required={true} required={true}
showResetButton={true}
defaultValue="7"
/> />
</div> </div>
)} )}
@@ -699,6 +706,8 @@ const Settings = ({
error={eliminationExtreme} error={eliminationExtreme}
warningMessage={t('warningEliminationOutOfRange')} warningMessage={t('warningEliminationOutOfRange')}
errorMessage={t('errorEliminationHalfLifeRequired')} errorMessage={t('errorEliminationHalfLifeRequired')}
showResetButton={true}
defaultValue="11"
/> />
</div> </div>
@@ -736,6 +745,8 @@ const Settings = ({
warning={conversionWarning} warning={conversionWarning}
warningMessage={t('warningConversionOutOfRange')} warningMessage={t('warningConversionOutOfRange')}
errorMessage={t('errorConversionHalfLifeRequired')} errorMessage={t('errorConversionHalfLifeRequired')}
showResetButton={true}
defaultValue="0.8"
/> />
</div> </div>
@@ -1144,16 +1155,6 @@ const Settings = ({
> >
{t('openDataManagement')} {t('openDataManagement')}
</Button> </Button>
{/* Reset Button - Always Visible */}
<Button
type="button"
onClick={onReset}
variant="destructive"
className="w-full"
>
{t('resetAllSettings')}
</Button>
</div> </div>
); );
}; };

View File

@@ -467,8 +467,8 @@ const SimulationChart = ({
// FIXME // FIXME
//label={{ value: t('axisLabelConcentration'), angle: -90, position: 'insideLeft', style: { fontStyle: 'italic', color: '#666' } }} //label={{ value: t('axisLabelConcentration'), angle: -90, position: 'insideLeft', style: { fontStyle: 'italic', color: '#666' } }}
domain={yAxisDomain as any} domain={yAxisDomain as any}
tickCount={20} tickCount={16}
interval={1} interval={0}
allowDecimals={false} allowDecimals={false}
allowDataOverflow={false} allowDataOverflow={false}
/> />

View File

@@ -9,7 +9,7 @@
*/ */
import * as React from "react" import * as React from "react"
import { Minus, Plus, X } from "lucide-react" import { Minus, Plus, RotateCcw } from "lucide-react"
import { Button } from "./button" import { Button } from "./button"
import { IconButtonWithTooltip } from "./icon-button-with-tooltip" import { IconButtonWithTooltip } from "./icon-button-with-tooltip"
import { Input } from "./input" import { Input } from "./input"
@@ -25,7 +25,8 @@ interface NumericInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
unit?: string unit?: string
align?: 'left' | 'center' | 'right' align?: 'left' | 'center' | 'right'
allowEmpty?: boolean allowEmpty?: boolean
clearButton?: boolean showResetButton?: boolean
defaultValue?: number | string
error?: boolean error?: boolean
warning?: boolean warning?: boolean
required?: boolean required?: boolean
@@ -44,7 +45,8 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
unit, unit,
align = 'right', align = 'right',
allowEmpty = false, allowEmpty = false,
clearButton = false, showResetButton = false,
defaultValue,
error = false, error = false,
warning = false, warning = false,
required = false, required = false,
@@ -217,7 +219,7 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
size="icon" size="icon"
className={cn( className={cn(
"h-9 w-9", "h-9 w-9",
clearButton && allowEmpty ? "rounded-l-none rounded-r-none border-x-0" : "rounded-l-none border-l-0", showResetButton ? "rounded-l-none rounded-r-none border-x-0" : "rounded-l-none border-l-0",
hasError && "border-destructive", hasError && "border-destructive",
hasWarning && !hasError && "border-yellow-500" hasWarning && !hasError && "border-yellow-500"
)} )}
@@ -226,11 +228,11 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
</Button> </Button>
{clearButton && allowEmpty && ( {showResetButton && (
<IconButtonWithTooltip <IconButtonWithTooltip
type="button" type="button"
icon={<X className="h-4 w-4" />} icon={<RotateCcw className="h-4 w-4" />}
tooltip={t('buttonClear')} tooltip={t('buttonResetToDefault')}
variant="outline" variant="outline"
size="icon" size="icon"
className={cn( className={cn(
@@ -238,7 +240,7 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
hasError && "border-destructive", hasError && "border-destructive",
hasWarning && !hasError && "border-yellow-500" hasWarning && !hasError && "border-yellow-500"
)} )}
onClick={() => onChange('')} onClick={() => onChange(String(defaultValue ?? ''))}
tabIndex={-1} tabIndex={-1}
/> />
)} )}

View File

@@ -151,15 +151,14 @@ export const getDefaultState = (): AppState => ({
id: 'day-template', id: 'day-template',
isTemplate: true, isTemplate: true,
doses: [ doses: [
{ id: 'dose-1', time: '06:30', ldx: '20' }, { id: 'dose-1', time: '06:30', ldx: '25' },
{ id: 'dose-2', time: '12:30', ldx: '10' }, { id: 'dose-2', time: '14:30', ldx: '15' },
{ id: 'dose-3', time: '17:30', ldx: '10' }, { id: 'dose-4', time: '22:15', ldx: '15' },
{ id: 'dose-4', time: '22:00', ldx: '7.5' },
] ]
} }
], ],
steadyStateConfig: { daysOnMedication: '7' }, // kept for backwards compatibility, now sourced from pkParams.advanced steadyStateConfig: { daysOnMedication: '7' }, // kept for backwards compatibility, now sourced from pkParams.advanced
therapeuticRange: { min: '10', max: '120' }, // min for adults and max for children for maximum range (users should personalize based on their response) therapeuticRange: { min: '', max: '' }, // users should personalize based on their response
doseIncrement: '2.5', doseIncrement: '2.5',
uiSettings: { uiSettings: {
showDayTimeOnXAxis: '24h', showDayTimeOnXAxis: '24h',

View File

@@ -311,13 +311,6 @@ export const useAppState = () => {
})); }));
}; };
const handleReset = () => {
if (window.confirm("Bist du sicher, dass du alle Einstellungen auf die Standardwerte zurücksetzen möchtest? Dies kann nicht rückgängig gemacht werden.")) {
window.localStorage.removeItem(LOCAL_STORAGE_KEY);
window.location.reload();
}
};
return { return {
appState, appState,
isLoaded, isLoaded,
@@ -331,7 +324,6 @@ export const useAppState = () => {
removeDoseFromDay, removeDoseFromDay,
updateDoseInDay, updateDoseInDay,
updateDoseFieldInDay, updateDoseFieldInDay,
sortDosesInDay, sortDosesInDay
handleReset
}; };
}; };

View File

@@ -210,6 +210,8 @@ export const de = {
exportOptionSimulation: "Simulations-Einstellungen (Dauer, Bereich, Diagrammansicht)", exportOptionSimulation: "Simulations-Einstellungen (Dauer, Bereich, Diagrammansicht)",
exportOptionPharmaco: "Pharmakokinetik-Einstellungen (Halbwertszeiten, therapeutischer Bereich)", exportOptionPharmaco: "Pharmakokinetik-Einstellungen (Halbwertszeiten, therapeutischer Bereich)",
exportOptionAdvanced: "Erweiterte Einstellungen (Gewicht, Nahrung, pH, Bioverfügbarkeit)", exportOptionAdvanced: "Erweiterte Einstellungen (Gewicht, Nahrung, pH, Bioverfügbarkeit)",
exportOptionOtherData: "Andere Daten (Design, eingeklappte Karten, Sprache, Haftungsausschluss)",
exportOptionOtherDataTooltip: "UI-Präferenzen wie Design, eingeklappte Kartenstatus, Spracheinstellung und Haftungsausschluss-Bestätigung. Normalerweise nicht nötig beim Teilen von Plänen mit anderen.",
exportButton: "Backup-Datei herunterladen", exportButton: "Backup-Datei herunterladen",
importButton: "Datei zum Importieren wählen", importButton: "Datei zum Importieren wählen",
importApplyButton: "Import anwenden", importApplyButton: "Import anwenden",
@@ -254,12 +256,24 @@ export const de = {
closeDataManagement: "Schließen", closeDataManagement: "Schließen",
pasteContentTooLarge: "Inhalt zu groß (max. 5000 Zeichen)", pasteContentTooLarge: "Inhalt zu groß (max. 5000 Zeichen)",
// Delete Data
deleteSpecificData: "Spezifische Daten löschen",
deleteSpecificDataTooltip: "Ausgewählte Datenkategorien dauerhaft von Ihrem Gerät löschen. Dieser Vorgang kann nicht rückgängig gemacht werden.",
deleteSelectWhat: "Was möchtest du löschen:",
deleteDataWarning: "⚠️ Warnung: Das Löschen ist dauerhaft und kann nicht rückgängig gemacht werden. Gelöschte Daten werden auf Standardwerte zurückgesetzt.",
deleteDataButton: "Ausgewählte Daten löschen",
deleteNoOptionsSelected: "Bitte wähle mindestens eine Kategorie zum Löschen aus.",
deleteDataConfirmTitle: "Bist du sicher, dass du die folgenden Daten dauerhaft löschen möchtest?",
deleteDataConfirmWarning: "Diese Aktion kann nicht rückgängig gemacht werden. Gelöschte Daten werden auf Werkseinstellungen zurückgesetzt.",
deleteDataSuccess: "Ausgewählte Daten wurden erfolgreich gelöscht.",
// 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.",
// Number input field // Number input field
buttonClear: "Feld löschen", buttonClear: "Feld löschen",
buttonResetToDefault: "Auf Standard zurücksetzen",
// Field validation - Errors // Field validation - Errors
errorNumberRequired: "⛔ Bitte gib eine gültige Zahl ein.", errorNumberRequired: "⛔ Bitte gib eine gültige Zahl ein.",

View File

@@ -208,6 +208,8 @@ export const en = {
exportOptionSimulation: "Simulation Settings (Duration, range, chart view)", exportOptionSimulation: "Simulation Settings (Duration, range, chart view)",
exportOptionPharmaco: "Pharmacokinetic Settings (Half-lives, therapeutic range)", exportOptionPharmaco: "Pharmacokinetic Settings (Half-lives, therapeutic range)",
exportOptionAdvanced: "Advanced Settings (Weight, food, pH, bioavailability)", exportOptionAdvanced: "Advanced Settings (Weight, food, pH, bioavailability)",
exportOptionOtherData: "Other Data (Theme, collapsed cards, language, disclaimer)",
exportOptionOtherDataTooltip: "UI preferences like theme, collapsed card states, language preference, and disclaimer acceptance. Typically not needed when sharing plans with others.",
exportButton: "Download Backup File", exportButton: "Download Backup File",
importButton: "Choose File to Import", importButton: "Choose File to Import",
importApplyButton: "Apply Import", importApplyButton: "Apply Import",
@@ -252,12 +254,24 @@ export const en = {
closeDataManagement: "Close", closeDataManagement: "Close",
pasteContentTooLarge: "Content too large (max. 5000 characters)", pasteContentTooLarge: "Content too large (max. 5000 characters)",
// Delete Data
deleteSpecificData: "Delete Specific Data",
deleteSpecificDataTooltip: "Permanently delete selected data categories from your device. This operation cannot be undone.",
deleteSelectWhat: "Select what to delete:",
deleteDataWarning: "⚠️ Warning: Deletion is permanent and cannot be undone. Deleted data will be reset to default values.",
deleteDataButton: "Delete Selected Data",
deleteNoOptionsSelected: "Please select at least one category to delete.",
deleteDataConfirmTitle: "Are you sure you want to permanently delete the following data?",
deleteDataConfirmWarning: "This action cannot be undone. Deleted data will be reset to factory defaults.",
deleteDataSuccess: "Selected data has been deleted successfully.",
// 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.",
// Number input field // Number input field
buttonClear: "Clear field", buttonClear: "Clear field",
buttonResetToDefault: "Reset to default",
// Field validation - Errors // Field validation - Errors
errorNumberRequired: "⛔ Please enter a valid number.", errorNumberRequired: "⛔ Please enter a valid number.",

View File

@@ -37,6 +37,13 @@ export interface ExportData {
doseIncrement: AppState['doseIncrement']; doseIncrement: AppState['doseIncrement'];
}; };
advancedSettings?: AppState['pkParams']['advanced']; advancedSettings?: AppState['pkParams']['advanced'];
otherData?: {
theme?: AppState['uiSettings']['theme'];
settingsCardStates?: any;
dayScheduleCollapsedStates?: any;
language?: string;
disclaimerAccepted?: boolean;
};
}; };
} }
@@ -46,6 +53,7 @@ export interface ExportOptions {
includeSimulationSettings: boolean; includeSimulationSettings: boolean;
includePharmacoSettings: boolean; includePharmacoSettings: boolean;
includeAdvancedSettings: boolean; includeAdvancedSettings: boolean;
includeOtherData: boolean;
} }
export interface ImportValidationResult { export interface ImportValidationResult {
@@ -111,6 +119,21 @@ export const exportSettings = (
exportData.data.advancedSettings = appState.pkParams.advanced; exportData.data.advancedSettings = appState.pkParams.advanced;
} }
if (options.includeOtherData) {
const settingsCardStates = localStorage.getItem('settingsCardStates_v1');
const dayScheduleCollapsedStates = localStorage.getItem('dayScheduleCollapsedDays_v1');
const language = localStorage.getItem('medPlanAssistant_language');
const disclaimerAccepted = localStorage.getItem('medPlanDisclaimerAccepted_v1');
exportData.data.otherData = {
theme: appState.uiSettings.theme,
settingsCardStates: settingsCardStates ? JSON.parse(settingsCardStates) : undefined,
dayScheduleCollapsedStates: dayScheduleCollapsedStates ? JSON.parse(dayScheduleCollapsedStates) : undefined,
language: language || undefined,
disclaimerAccepted: disclaimerAccepted === 'true',
};
}
return exportData; return exportData;
}; };
@@ -228,6 +251,17 @@ export const validateImportData = (data: any): ImportValidationResult => {
} }
} }
// Validate other data
if (importData.otherData !== undefined) {
const validFields = ['theme', 'settingsCardStates', 'dayScheduleCollapsedStates', 'language', 'disclaimerAccepted'];
const importedFields = Object.keys(importData.otherData);
const unknownFields = importedFields.filter(f => !validFields.includes(f));
if (unknownFields.length > 0) {
result.warnings.push(`Other data: Unknown fields found (${unknownFields.join(', ')})`);
result.hasUnknownFields = true;
}
}
return result; return result;
}; };
@@ -295,6 +329,131 @@ export const importSettings = (
}; };
} }
if (options.includeOtherData && importData.otherData) {
// Update theme in uiSettings
if (importData.otherData.theme !== undefined) {
if (!newState.uiSettings) {
newState.uiSettings = { ...currentState.uiSettings };
}
newState.uiSettings.theme = importData.otherData.theme;
}
// Update localStorage-only settings
if (importData.otherData.settingsCardStates !== undefined) {
localStorage.setItem('settingsCardStates_v1', JSON.stringify(importData.otherData.settingsCardStates));
}
if (importData.otherData.dayScheduleCollapsedStates !== undefined) {
localStorage.setItem('dayScheduleCollapsedDays_v1', JSON.stringify(importData.otherData.dayScheduleCollapsedStates));
}
if (importData.otherData.language !== undefined) {
localStorage.setItem('medPlanAssistant_language', importData.otherData.language);
}
if (importData.otherData.disclaimerAccepted !== undefined) {
localStorage.setItem('medPlanDisclaimerAccepted_v1', importData.otherData.disclaimerAccepted ? 'true' : 'false');
}
}
return newState;
};
/**
* Delete selected data categories from localStorage and return updated state
* @param currentState Current application state
* @param options Which categories to delete
* @returns Partial state with defaults for deleted categories
*/
export const deleteSelectedData = (
currentState: AppState,
options: ExportOptions
): Partial<AppState> => {
const defaults = getDefaultState();
const newState: Partial<AppState> = {};
// Track if main localStorage should be removed
let shouldRemoveMainStorage = false;
if (options.includeSchedules) {
// Delete schedules - but always keep template day with at least one dose
// Never allow complete deletion as this breaks the app
const defaults = getDefaultState();
newState.days = [
{
id: 'day-template',
isTemplate: true,
doses: [
{ id: 'dose-default', time: '06:00', ldx: '70' }
]
}
];
shouldRemoveMainStorage = true;
}
if (options.includeDiagramSettings) {
if (!newState.uiSettings) {
newState.uiSettings = { ...currentState.uiSettings };
}
// Reset diagram settings to defaults
newState.uiSettings.showDayTimeOnXAxis = defaults.uiSettings.showDayTimeOnXAxis;
newState.uiSettings.showTemplateDay = defaults.uiSettings.showTemplateDay;
newState.uiSettings.showDayReferenceLines = defaults.uiSettings.showDayReferenceLines;
newState.uiSettings.showTherapeuticRange = defaults.uiSettings.showTherapeuticRange;
newState.uiSettings.stickyChart = defaults.uiSettings.stickyChart;
shouldRemoveMainStorage = true;
}
if (options.includeSimulationSettings) {
if (!newState.uiSettings) {
newState.uiSettings = { ...currentState.uiSettings };
}
// Reset simulation settings to defaults
newState.uiSettings.simulationDays = defaults.uiSettings.simulationDays;
newState.uiSettings.displayedDays = defaults.uiSettings.displayedDays;
newState.uiSettings.yAxisMin = defaults.uiSettings.yAxisMin;
newState.uiSettings.yAxisMax = defaults.uiSettings.yAxisMax;
newState.uiSettings.chartView = defaults.uiSettings.chartView;
newState.uiSettings.steadyStateDaysEnabled = defaults.uiSettings.steadyStateDaysEnabled;
shouldRemoveMainStorage = true;
}
if (options.includePharmacoSettings) {
// Reset pharmacokinetic settings to defaults
newState.pkParams = {
...currentState.pkParams,
ldx: defaults.pkParams.ldx,
damph: defaults.pkParams.damph,
};
newState.therapeuticRange = defaults.therapeuticRange;
newState.doseIncrement = defaults.doseIncrement;
shouldRemoveMainStorage = true;
}
if (options.includeAdvancedSettings) {
if (!newState.pkParams) {
newState.pkParams = { ...currentState.pkParams };
}
// Reset advanced settings to defaults
newState.pkParams.advanced = defaults.pkParams.advanced;
shouldRemoveMainStorage = true;
}
if (options.includeOtherData) {
// Reset theme to default
if (!newState.uiSettings) {
newState.uiSettings = { ...currentState.uiSettings };
}
newState.uiSettings.theme = defaults.uiSettings.theme;
// Remove UI state from localStorage
localStorage.removeItem('settingsCardStates_v1');
localStorage.removeItem('dayScheduleCollapsedDays_v1');
localStorage.removeItem('medPlanAssistant_language');
localStorage.removeItem('medPlanDisclaimerAccepted_v1');
shouldRemoveMainStorage = true;
}
// If any main state category was deleted, we'll trigger a save by returning the partial state
// The useAppState hook will handle saving to localStorage
return newState; return newState;
}; };