diff --git a/web/normogen-web/package-lock.json b/web/normogen-web/package-lock.json
index c189c7c..8cafad0 100644
--- a/web/normogen-web/package-lock.json
+++ b/web/normogen-web/package-lock.json
@@ -10,7 +10,6 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
- "@mui/icons-material": "^7.3.11",
"@mui/material": "^7.3.9",
"@mui/x-charts": "^8.27.4",
"axios": "^1.13.6",
@@ -1128,52 +1127,26 @@
}
},
"node_modules/@mui/core-downloads-tracker": {
- "version": "7.3.11",
- "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.11.tgz",
- "integrity": "sha512-a7I/b/nBTdXYz2cOSlEmkQ9WWE1x8FHpqMhFPp+Y1VPFxcOw91G5ELOHARQAGSPy5V+UCgJua6K/1x70bAtQPw==",
+ "version": "7.3.9",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.9.tgz",
+ "integrity": "sha512-MOkOCTfbMJwLshlBCKJ59V2F/uaLYfmKnN76kksj6jlGUVdI25A9Hzs08m+zjBRdLv+sK7Rqdsefe8X7h/6PCw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
- "node_modules/@mui/icons-material": {
- "version": "7.3.11",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.11.tgz",
- "integrity": "sha512-+hz5ilwHZ3djd5es3sCErLioqe/NhZcYTsV/TNXZAMdJdb23F4xzJjqnnZdnurc3S1+ietcssRNqieOhPQLZ7Q==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.28.6"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@mui/material": "^7.3.11",
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- }
- }
- },
"node_modules/@mui/material": {
- "version": "7.3.11",
- "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.11.tgz",
- "integrity": "sha512-yq8bPc3LxOwKRWpcjRgDkYFmpM6aKlARfESTmOQcvLYFeJwtHte2tw6hJDrb8sk8wcvpDprHEHVaoUU0MslIkw==",
+ "version": "7.3.9",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.9.tgz",
+ "integrity": "sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
- "@mui/core-downloads-tracker": "^7.3.11",
- "@mui/system": "^7.3.11",
+ "@mui/core-downloads-tracker": "^7.3.9",
+ "@mui/system": "^7.3.9",
"@mui/types": "^7.4.12",
- "@mui/utils": "^7.3.11",
+ "@mui/utils": "^7.3.9",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
@@ -1192,7 +1165,7 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
- "@mui/material-pigment-css": "^7.3.11",
+ "@mui/material-pigment-css": "^7.3.9",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1219,13 +1192,13 @@
"license": "MIT"
},
"node_modules/@mui/private-theming": {
- "version": "7.3.11",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.11.tgz",
- "integrity": "sha512-9B+YKms0fRHbNrqp9tOT/DNbNnU5gyvJ1o3qAGXfq8GmZcbJnE3At9x07Zr/o0pkhzg4aDdwXVqe4+AcgtOCPA==",
+ "version": "7.3.9",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.9.tgz",
+ "integrity": "sha512-ErIyRQvsiQEq7Yvcvfw9UDHngaqjMy9P3JDPnRAaKG5qhpl2C4tX/W1S4zJvpu+feihmZJStjIyvnv6KDbIrlw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
- "@mui/utils": "^7.3.11",
+ "@mui/utils": "^7.3.9",
"prop-types": "^15.8.1"
},
"engines": {
@@ -1246,9 +1219,9 @@
}
},
"node_modules/@mui/styled-engine": {
- "version": "7.3.10",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.10.tgz",
- "integrity": "sha512-WxE9SiF8xskAQqGjsp0poXCkCqsoXFEsSr0HBXfApmGHR+DBnXRp+z46Vsltg4gpPM4Z96DeAQRpeAOnhNg7Ng==",
+ "version": "7.3.9",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.9.tgz",
+ "integrity": "sha512-JqujWt5bX4okjUPGpVof/7pvgClqh7HvIbsIBIOOlCh2u3wG/Bwp4+E1bc1dXSwkrkp9WUAoNdI5HEC+5HKvMw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
@@ -1280,16 +1253,16 @@
}
},
"node_modules/@mui/system": {
- "version": "7.3.11",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.11.tgz",
- "integrity": "sha512-7izwGWdNawAKpBKcRlx7f2gFnAAjmASBWvMcyX4YYEeLOFsbfGRbUYGInvnAcUeql3rPxI7F9Ft4oY2OLRz44g==",
+ "version": "7.3.9",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.9.tgz",
+ "integrity": "sha512-aL1q9am8XpRrSabv9qWf5RHhJICJql34wnrc1nz0MuOglPRYF/liN+c8VqZdTvUn9qg+ZjRVbKf4sJVFfIDtmg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
- "@mui/private-theming": "^7.3.11",
- "@mui/styled-engine": "^7.3.10",
+ "@mui/private-theming": "^7.3.9",
+ "@mui/styled-engine": "^7.3.9",
"@mui/types": "^7.4.12",
- "@mui/utils": "^7.3.11",
+ "@mui/utils": "^7.3.9",
"clsx": "^2.1.1",
"csstype": "^3.2.3",
"prop-types": "^15.8.1"
@@ -1337,9 +1310,9 @@
}
},
"node_modules/@mui/utils": {
- "version": "7.3.11",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
- "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
+ "version": "7.3.9",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.9.tgz",
+ "integrity": "sha512-U6SdZaGbfb65fqTsH3V5oJdFj9uYwyLE2WVuNvmbggTSDBb8QHrFsqY8BN3taK9t3yJ8/BPHD/kNvLNyjwM7Yw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.6",
diff --git a/web/normogen-web/package.json b/web/normogen-web/package.json
index fd4c3cc..f9501e2 100644
--- a/web/normogen-web/package.json
+++ b/web/normogen-web/package.json
@@ -13,7 +13,6 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
- "@mui/icons-material": "^7.3.11",
"@mui/material": "^7.3.9",
"@mui/x-charts": "^8.27.4",
"axios": "^1.13.6",
diff --git a/web/normogen-web/src/components/common/SeverityChip.tsx b/web/normogen-web/src/components/common/SeverityChip.tsx
deleted file mode 100644
index 32e9e29..0000000
--- a/web/normogen-web/src/components/common/SeverityChip.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Chip } from '@mui/material';
-import { InteractionSeverity } from '../../types/api';
-
-const CONFIG: Record<
- InteractionSeverity,
- { color: 'error' | 'warning' | 'success' | 'default'; label: string }
-> = {
- [InteractionSeverity.Severe]: { color: 'error', label: 'Severe' },
- [InteractionSeverity.Moderate]: { color: 'warning', label: 'Moderate' },
- [InteractionSeverity.Mild]: { color: 'success', label: 'Mild' },
- [InteractionSeverity.Unknown]: { color: 'default', label: 'Unknown' },
-};
-
-export const SeverityChip = ({ severity }: { severity: InteractionSeverity }) => {
- const cfg = CONFIG[severity] ?? CONFIG[InteractionSeverity.Unknown];
- return ;
-};
diff --git a/web/normogen-web/src/components/health/HealthStats.tsx b/web/normogen-web/src/components/health/HealthStats.tsx
deleted file mode 100644
index a76284d..0000000
--- a/web/normogen-web/src/components/health/HealthStats.tsx
+++ /dev/null
@@ -1,309 +0,0 @@
-import { useEffect, useState, type FC } from 'react';
-import {
- Box,
- Button,
- Card,
- CardContent,
- CircularProgress,
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
- Alert,
- MenuItem,
- Stack,
- TextField,
- Typography,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
-} from '@mui/material';
-import AddIcon from '@mui/icons-material/Add';
-import TrendUpIcon from '@mui/icons-material/TrendingUp';
-import TrendDownIcon from '@mui/icons-material/TrendingDown';
-import TrendFlatIcon from '@mui/icons-material/TrendingFlat';
-import {
- LineChart,
- Line,
- XAxis,
- YAxis,
- CartesianGrid,
- Tooltip,
- ResponsiveContainer,
-} from 'recharts';
-import { format } from 'date-fns';
-import { useHealthStore } from '../../store/useStore';
-import { HealthStatType, type CreateHealthStatRequest, type TrendData } from '../../types/api';
-
-const STAT_TYPES = Object.values(HealthStatType);
-
-const UNIT_DEFAULTS: Record = {
- [HealthStatType.Weight]: 'kg',
- [HealthStatType.Height]: 'cm',
- [HealthStatType.BloodPressure]: 'mmHg',
- [HealthStatType.HeartRate]: 'bpm',
- [HealthStatType.BloodSugar]: 'mg/dL',
- [HealthStatType.Temperature]: '°C',
- [HealthStatType.OxygenSaturation]: '%',
- [HealthStatType.Cholesterol]: 'mg/dL',
- [HealthStatType.Other]: '',
-};
-
-const trendIcon = (trend: TrendData['trend']) => {
- if (trend === 'up') return ;
- if (trend === 'down') return ;
- return ;
-};
-
-const toLocalInput = (d: Date) => {
- // datetime-local expects YYYY-MM-DDTHH:mm in local time.
- const pad = (n: number) => String(n).padStart(2, '0');
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
-};
-
-export const HealthStats: FC = () => {
- const { stats, trends, isLoading, error, loadStats, loadTrends, createStat, clearError } =
- useHealthStore();
-
- const [addOpen, setAddOpen] = useState(false);
- const [saving, setSaving] = useState(false);
- const [chartType, setChartType] = useState(HealthStatType.Weight);
-
- const [form, setForm] = useState({
- stat_type: HealthStatType.Weight,
- value: 0,
- unit: UNIT_DEFAULTS[HealthStatType.Weight],
- measured_at: new Date().toISOString(),
- });
-
- useEffect(() => {
- loadStats();
- loadTrends();
- }, [loadStats, loadTrends]);
-
- const openAdd = () => {
- const t = HealthStatType.Weight;
- setForm({
- stat_type: t,
- value: 0,
- unit: UNIT_DEFAULTS[t],
- measured_at: new Date().toISOString(),
- });
- setAddOpen(true);
- };
-
- const submitAdd = async () => {
- setSaving(true);
- try {
- await createStat(form);
- setAddOpen(false);
- } catch {
- /* error surfaced via store */
- } finally {
- setSaving(false);
- }
- };
-
- const typedTrends = trends as TrendData[];
- const chartData = stats
- .filter((s) => s.stat_type === chartType)
- .sort((a, b) => new Date(a.measured_at).getTime() - new Date(b.measured_at).getTime())
- .map((s) => ({ time: format(new Date(s.measured_at), 'MMM d'), value: s.value }));
-
- return (
-
-
- Health statistics
- } onClick={openAdd}>
- Record
-
-
-
- {error && (
-
- {error}
-
- )}
-
- {isLoading ? (
-
-
-
- ) : (
- <>
- {/* Trend summary cards */}
- {typedTrends.length > 0 && (
-
- {typedTrends.map((t) => (
-
-
-
- {t.stat_type}
-
-
- {t.average.toFixed(1)}
- {trendIcon(t.trend)}
-
-
- {t.min} – {t.max}
-
-
-
- ))}
-
- )}
-
- {/* Chart with type selector */}
-
-
-
- setChartType(e.target.value as HealthStatType)}
- sx={{ minWidth: 180 }}
- >
- {STAT_TYPES.map((t) => (
-
- ))}
-
-
- {chartData.length === 0 ? (
-
- No {chartType} readings recorded yet.
-
- ) : (
-
-
-
-
-
-
-
-
-
- )}
-
-
-
- {/* Recent readings table */}
- {stats.length > 0 && (
-
-
-
-
- Type
- Value
- When
- Notes
-
-
-
- {[...stats]
- .sort(
- (a, b) =>
- new Date(b.measured_at).getTime() - new Date(a.measured_at).getTime(),
- )
- .slice(0, 15)
- .map((s) => (
-
- {s.stat_type}
-
- {s.value} {s.unit}
-
- {format(new Date(s.measured_at), 'MMM d, HH:mm')}
- {s.notes ?? ''}
-
- ))}
-
-
-
- )}
-
- {stats.length === 0 && typedTrends.length === 0 && (
-
- No health readings yet — record one.
-
- )}
- >
- )}
-
- {/* Add dialog */}
-
-
- );
-};
-
-export default HealthStats;
diff --git a/web/normogen-web/src/components/interactions/InteractionsChecker.tsx b/web/normogen-web/src/components/interactions/InteractionsChecker.tsx
deleted file mode 100644
index aeee0b1..0000000
--- a/web/normogen-web/src/components/interactions/InteractionsChecker.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import { useState, type FC } from 'react';
-import {
- Box,
- Button,
- Card,
- CardContent,
- CircularProgress,
- Alert,
- Stack,
- Typography,
- Chip,
-} from '@mui/material';
-import WarningAmberIcon from '@mui/icons-material/WarningAmber';
-import CheckCircleIcon from '@mui/icons-material/CheckCircle';
-import { useInteractionStore, useMedicationStore } from '../../store/useStore';
-import { SeverityChip } from '../common/SeverityChip';
-
-export const InteractionsChecker: FC = () => {
- const { medications } = useMedicationStore();
- const { interactions, isChecking, error, checkInteractions, clearError } =
- useInteractionStore();
-
- const [selected, setSelected] = useState([]);
-
- const toggle = (name: string) => {
- setSelected((prev) =>
- prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name],
- );
- };
-
- const runCheck = async () => {
- if (selected.length < 2) return;
- await checkInteractions(selected);
- };
-
- const hasChecked = interactions.length > 0 || !isChecking;
-
- return (
-
-
- Drug interactions
-
-
- Select two or more of your medications to check for known interactions.
-
-
- {error && (
-
- {error}
-
- )}
-
- {medications.length === 0 ? (
-
- Add medications first (Medications tab), then check them for interactions.
-
- ) : (
- <>
-
- {medications.map((m) => {
- const on = selected.includes(m.name);
- return (
- toggle(m.name)}
- />
- );
- })}
-
-
-
-
- {/* Results */}
- {hasChecked && !isChecking && selected.length >= 2 && (
- <>
- {interactions.length === 0 ? (
- } severity="success">
- No known interactions found between the selected medications.
-
- ) : (
-
- } severity="warning">
- {interactions.length} interaction{interactions.length > 1 ? 's' : ''} found.
-
- {interactions.map((ix, i) => (
-
-
-
-
- {ix.medications.join(' + ')}
-
-
-
-
- {ix.description}
-
- {ix.disclaimer && (
-
- {ix.disclaimer}
-
- )}
-
-
- ))}
-
- )}
- >
- )}
- >
- )}
-
- );
-};
-
-export default InteractionsChecker;
diff --git a/web/normogen-web/src/components/medication/MedicationManager.tsx b/web/normogen-web/src/components/medication/MedicationManager.tsx
deleted file mode 100644
index 8c9dc62..0000000
--- a/web/normogen-web/src/components/medication/MedicationManager.tsx
+++ /dev/null
@@ -1,348 +0,0 @@
-import { useEffect, useState, type FC } from 'react';
-import {
- Box,
- Button,
- Card,
- CardContent,
- CardActions,
- Chip,
- CircularProgress,
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
- Alert,
- IconButton,
- List,
- ListItem,
- ListItemText,
- MenuItem,
- Stack,
- TextField,
- Typography,
-} from '@mui/material';
-import EditIcon from '@mui/icons-material/Edit';
-import DeleteIcon from '@mui/icons-material/Delete';
-import AddIcon from '@mui/icons-material/Add';
-import { useMedicationStore, useAuthStore } from '../../store/useStore';
-import type {
- Medication,
- CreateMedicationRequest,
- UpdateMedicationRequest,
-} from '../../types/api';
-
-const ROUTES = ['oral', 'topical', 'injection', 'inhalation', 'other'] as const;
-
-const emptyCreate: CreateMedicationRequest = {
- name: '',
- dosage: '',
- frequency: '',
- route: 'oral',
- profile_id: 'default',
-};
-
-export const MedicationManager: FC = () => {
- const {
- medications,
- isLoading,
- error,
- loadMedications,
- createMedication,
- updateMedication,
- deleteMedication,
- clearError,
- } = useMedicationStore();
- const user = useAuthStore((s) => s.user);
-
- const [createOpen, setCreateOpen] = useState(false);
- const [createForm, setCreateForm] = useState(emptyCreate);
- const [editTarget, setEditTarget] = useState(null);
- const [editForm, setEditForm] = useState({});
- const [deleteTarget, setDeleteTarget] = useState(null);
- const [saving, setSaving] = useState(false);
-
- useEffect(() => {
- loadMedications();
- }, [loadMedications]);
-
- const openCreate = () => {
- // TODO: real profile management — for now, source profile_id from the user,
- // falling back to 'default' (the backend accepts any string).
- setCreateForm({
- ...emptyCreate,
- profile_id: user?.profile_id ?? 'default',
- });
- setCreateOpen(true);
- };
-
- const submitCreate = async () => {
- setSaving(true);
- try {
- await createMedication(createForm);
- setCreateOpen(false);
- } catch {
- /* error surfaced via store */
- } finally {
- setSaving(false);
- }
- };
-
- const openEdit = (med: Medication) => {
- setEditTarget(med);
- setEditForm({
- name: med.name,
- dosage: med.dosage,
- frequency: med.frequency,
- instructions: med.instructions,
- active: med.active,
- });
- };
-
- const submitEdit = async () => {
- if (!editTarget?.medication_id) return;
- setSaving(true);
- try {
- await updateMedication(editTarget.medication_id, editForm);
- setEditTarget(null);
- } catch {
- /* error surfaced via store */
- } finally {
- setSaving(false);
- }
- };
-
- const confirmDelete = async () => {
- if (!deleteTarget?.medication_id) return;
- setSaving(true);
- try {
- await deleteMedication(deleteTarget.medication_id);
- setDeleteTarget(null);
- } catch {
- /* error surfaced via store */
- } finally {
- setSaving(false);
- }
- };
-
- return (
-
-
- Medications
- } onClick={openCreate}>
- Add
-
-
-
- {error && (
-
- {error}
-
- )}
-
- {isLoading ? (
-
-
-
- ) : medications.length === 0 ? (
-
- No medications yet — add one.
-
- ) : (
-
- {medications.map((med) => (
-
-
-
-
-
-
- {med.name}
- {' '}
-
- {med.dosage} · {med.frequency}
-
-
-
-
- {med.instructions && (
-
- {med.instructions}
-
- )}
-
-
-
-
- openEdit(med)} aria-label="edit">
-
-
- setDeleteTarget(med)}
- aria-label="delete"
- color="error"
- >
-
-
-
-
-
- ))}
-
- )}
-
- {/* Create dialog */}
-
-
- {/* Edit dialog */}
-
-
- {/* Delete confirm */}
-
-
- );
-};
-
-export default MedicationManager;
diff --git a/web/normogen-web/src/pages/Dashboard.tsx b/web/normogen-web/src/pages/Dashboard.tsx
index fb139f7..7e85b83 100644
--- a/web/normogen-web/src/pages/Dashboard.tsx
+++ b/web/normogen-web/src/pages/Dashboard.tsx
@@ -1,20 +1,24 @@
-import { useEffect, useState, type FC } from 'react';
+import { useEffect, type FC } from 'react';
import { useNavigate } from 'react-router-dom';
-import { AppBar, Toolbar, Typography, Button, Container, Box, Tabs, Tab } from '@mui/material';
+import {
+ AppBar,
+ Toolbar,
+ Typography,
+ Button,
+ Container,
+ Card,
+ CardContent,
+ Box,
+ CircularProgress,
+} from '@mui/material';
import { useAuthStore } from '../store/useStore';
-import { MedicationManager } from '../components/medication/MedicationManager';
-import { HealthStats } from '../components/health/HealthStats';
-import { InteractionsChecker } from '../components/interactions/InteractionsChecker';
-
-type TabIndex = 0 | 1 | 2;
export const Dashboard: FC = () => {
const navigate = useNavigate();
- const { user, isAuthenticated, loadUser, logout } = useAuthStore();
- const [tab, setTab] = useState(0);
+ const { user, isAuthenticated, isLoading, loadUser, logout } = useAuthStore();
// On mount, refresh the user record from the backend (confirms the token is
- // still valid, and medication-create needs user.profile_id).
+ // still valid and pulls the latest profile).
useEffect(() => {
if (isAuthenticated && !user) {
loadUser();
@@ -33,32 +37,34 @@ export const Dashboard: FC = () => {
Normogen
- {user && (
-
- {user.username}
-
- )}
-
- setTab(v as TabIndex)} sx={{ mb: 3 }}>
-
-
-
-
+
+ {isLoading && (
+
+
+
+ )}
-
- {tab === 0 && }
- {tab === 1 && }
- {tab === 2 && }
-
+
+
+
+ Welcome{user ? `, ${user.username}` : ''}
+
+
+ You're signed in to Normogen.
+
+
+ This is the dashboard shell. Medication management, health statistics,
+ and drug-interaction checking UIs will be built here.
+
+
+
>
);
};
-
-export default Dashboard;