274 lines
9.6 KiB
TypeScript
274 lines
9.6 KiB
TypeScript
/**
|
|
* Medication Plan Assistant - Main Application
|
|
*
|
|
* A pharmacokinetic simulation tool for lisdexamfetamine (Elvanse/Vyvanse)
|
|
* medication planning. Helps users visualize drug concentration profiles,
|
|
* manage deviations, and get dose correction suggestions.
|
|
*
|
|
* @author Andreas Weyer
|
|
* @license MIT
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { GitBranch, Pin, PinOff } from 'lucide-react';
|
|
|
|
// Components
|
|
import DaySchedule from './components/day-schedule';
|
|
import SimulationChart from './components/simulation-chart';
|
|
import Settings from './components/settings';
|
|
import LanguageSelector from './components/language-selector';
|
|
import DisclaimerModal from './components/disclaimer-modal';
|
|
import { Button } from './components/ui/button';
|
|
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from './components/ui/tooltip';
|
|
import { IconButtonWithTooltip } from './components/ui/icon-button-with-tooltip';
|
|
import { PROJECT_REPOSITORY_URL, APP_VERSION } from './constants/defaults';
|
|
|
|
// Custom Hooks
|
|
import { useAppState } from './hooks/useAppState';
|
|
import { useSimulation } from './hooks/useSimulation';
|
|
import { useLanguage } from './hooks/useLanguage';
|
|
|
|
// --- Main Component ---
|
|
const MedPlanAssistant = () => {
|
|
const { currentLanguage, t, changeLanguage } = useLanguage();
|
|
|
|
// Disclaimer modal state
|
|
const [showDisclaimer, setShowDisclaimer] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
const hasAccepted = localStorage.getItem('medPlanDisclaimerAccepted_v1');
|
|
if (!hasAccepted) {
|
|
setShowDisclaimer(true);
|
|
}
|
|
}, []);
|
|
|
|
const handleAcceptDisclaimer = () => {
|
|
localStorage.setItem('medPlanDisclaimerAccepted_v1', 'true');
|
|
setShowDisclaimer(false);
|
|
};
|
|
|
|
const handleOpenDisclaimer = () => {
|
|
setShowDisclaimer(true);
|
|
};
|
|
|
|
// Use shorter button labels on narrow screens to keep the pin control visible
|
|
const [useCompactButtons, setUseCompactButtons] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
const updateCompact = () => {
|
|
setUseCompactButtons(window.innerWidth < 520); // tweakable threshold
|
|
};
|
|
|
|
updateCompact();
|
|
window.addEventListener('resize', updateCompact);
|
|
return () => window.removeEventListener('resize', updateCompact);
|
|
}, []);
|
|
|
|
const {
|
|
appState,
|
|
updateState,
|
|
updateNestedState,
|
|
updateUiSetting,
|
|
handleReset,
|
|
addDay,
|
|
removeDay,
|
|
addDoseToDay,
|
|
removeDoseFromDay,
|
|
updateDoseInDay,
|
|
sortDosesInDay
|
|
} = useAppState();
|
|
|
|
const {
|
|
pkParams,
|
|
days,
|
|
therapeuticRange,
|
|
doseIncrement,
|
|
uiSettings
|
|
} = appState;
|
|
|
|
const {
|
|
showDayTimeOnXAxis,
|
|
chartView,
|
|
yAxisMin,
|
|
yAxisMax,
|
|
showTemplateDay,
|
|
simulationDays,
|
|
displayedDays,
|
|
showDayReferenceLines
|
|
} = uiSettings;
|
|
|
|
const {
|
|
combinedProfile,
|
|
templateProfile
|
|
} = useSimulation(appState);
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<div className="min-h-screen bg-background p-4 sm:p-6 lg:p-8">
|
|
{/* Disclaimer Modal */}
|
|
<DisclaimerModal
|
|
isOpen={showDisclaimer}
|
|
onAccept={handleAcceptDisclaimer}
|
|
currentLanguage={currentLanguage}
|
|
onLanguageChange={changeLanguage}
|
|
t={t}
|
|
/>
|
|
|
|
<div className="max-w-7xl mx-auto">
|
|
<header className="mb-8">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h1 className="text-3xl md:text-4xl font-bold tracking-tight">{t('appTitle')}</h1>
|
|
</div>
|
|
<LanguageSelector currentLanguage={currentLanguage} onLanguageChange={changeLanguage} t={t} />
|
|
</div>
|
|
<p className="text-muted-foreground mt-1">{t('appSubtitle')}</p>
|
|
</header>
|
|
|
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
|
|
|
{/* Both Columns - Chart */}
|
|
<div className={`xl:col-span-2 bg-card p-6 rounded-lg border min-h-[600px] flex flex-col ${uiSettings.stickyChart ? 'sticky top-2 z-30 shadow-lg' : ''}`}
|
|
style={uiSettings.stickyChart ? { borderColor: 'hsl(var(--primary))' } : {}}>
|
|
<div className="flex flex-wrap items-center gap-3 justify-between mb-4">
|
|
<div className="flex flex-wrap justify-center gap-2">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
onClick={() => updateUiSetting('chartView', 'damph')}
|
|
variant={chartView === 'damph' ? 'default' : 'secondary'}
|
|
>
|
|
{t(useCompactButtons ? 'dAmphetamineShort' : 'dAmphetamine')}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p className="text-xs max-w-xs">{t('chartViewDamphTooltip')}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
onClick={() => updateUiSetting('chartView', 'ldx')}
|
|
variant={chartView === 'ldx' ? 'default' : 'secondary'}
|
|
>
|
|
{t(useCompactButtons ? 'lisdexamfetamineShort' : 'lisdexamfetamine')}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p className="text-xs max-w-xs">{t('chartViewLdxTooltip')}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
onClick={() => updateUiSetting('chartView', 'both')}
|
|
variant={chartView === 'both' ? 'default' : 'secondary'}
|
|
>
|
|
{t('both')}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p className="text-xs max-w-xs">{t('chartViewBothTooltip')}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
<IconButtonWithTooltip
|
|
onClick={() => updateUiSetting('stickyChart', !uiSettings.stickyChart)}
|
|
icon={uiSettings.stickyChart ? <Pin size={16} /> : <PinOff size={16} />}
|
|
tooltip={uiSettings.stickyChart ? t('unpinChart') : t('pinChart')}
|
|
variant={uiSettings.stickyChart ? 'default' : 'outline'}
|
|
size="sm"
|
|
className="shrink-0"
|
|
/>
|
|
</div>
|
|
|
|
<SimulationChart
|
|
combinedProfile={combinedProfile}
|
|
templateProfile={showTemplateDay ? templateProfile : null}
|
|
chartView={chartView}
|
|
showDayTimeOnXAxis={showDayTimeOnXAxis}
|
|
showDayReferenceLines={showDayReferenceLines}
|
|
showTherapeuticRange={uiSettings.showTherapeuticRange ?? true}
|
|
therapeuticRange={therapeuticRange}
|
|
simulationDays={simulationDays}
|
|
displayedDays={displayedDays}
|
|
yAxisMin={yAxisMin}
|
|
yAxisMax={yAxisMax}
|
|
days={days}
|
|
t={t}
|
|
/>
|
|
</div>
|
|
|
|
{/* Left Column - Controls */}
|
|
<div className="xl:col-span-1 space-y-6">
|
|
<DaySchedule
|
|
days={days}
|
|
doseIncrement={doseIncrement}
|
|
onAddDay={addDay}
|
|
onRemoveDay={removeDay}
|
|
onAddDose={addDoseToDay}
|
|
onRemoveDose={removeDoseFromDay}
|
|
onUpdateDose={updateDoseInDay}
|
|
onSortDoses={sortDosesInDay}
|
|
t={t}
|
|
/>
|
|
</div>
|
|
|
|
{/* Right Column - Settings */}
|
|
<div className="xl:col-span-1 space-y-6">
|
|
<Settings
|
|
pkParams={pkParams}
|
|
therapeuticRange={therapeuticRange}
|
|
uiSettings={uiSettings}
|
|
days={days}
|
|
doseIncrement={doseIncrement}
|
|
onUpdatePkParams={(key: any, value: any) => updateNestedState('pkParams', key, value)}
|
|
onUpdateTherapeuticRange={(key: any, value: any) => updateNestedState('therapeuticRange', key, value)}
|
|
onUpdateUiSetting={updateUiSetting}
|
|
onReset={handleReset}
|
|
onImportDays={(importedDays: any) => updateState('days', importedDays)}
|
|
t={t}
|
|
/>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<footer className="mt-8 p-4 bg-muted rounded-lg text-sm text-muted-foreground border">
|
|
<div className="space-y-3">
|
|
<div>
|
|
<h3 className="font-semibold mb-2 text-foreground">{t('importantNote')}</h3>
|
|
<p>{t('disclaimer')}</p>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleOpenDisclaimer}
|
|
>
|
|
{t('disclaimerModalFooterLink')}
|
|
</Button>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-xs text-muted-foreground" title={`Version: ${APP_VERSION}${APP_VERSION.endsWith('-dirty') ? ' (uncommitted changes)' : ''}`}>
|
|
v{APP_VERSION}
|
|
</span>
|
|
<a
|
|
href={PROJECT_REPOSITORY_URL}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center justify-center w-8 h-8 rounded-md hover:bg-accent text-foreground hover:text-accent-foreground transition-colors"
|
|
title={t('footerProjectRepo')}
|
|
>
|
|
<GitBranch size={18} />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
</TooltipProvider>
|
|
);
|
|
};
|
|
|
|
export default MedPlanAssistant;
|