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 - - - - {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) => ( - - {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 */} - setAddOpen(false)} fullWidth maxWidth="sm"> - Record a measurement - - - { - const t = e.target.value as HealthStatType; - setForm({ ...form, stat_type: t, unit: UNIT_DEFAULTS[t] }); - }} - > - {STAT_TYPES.map((t) => ( - - {t} - - ))} - - - setForm({ ...form, value: Number(e.target.value) })} - /> - setForm({ ...form, unit: e.target.value })} - /> - - - setForm({ ...form, measured_at: new Date(e.target.value).toISOString() }) - } - /> - setForm({ ...form, notes: e.target.value || undefined })} - /> - - - - - - - -
- ); -}; - -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 - - - - {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 */} - setCreateOpen(false)} fullWidth maxWidth="sm"> - Add medication - - - setCreateForm({ ...createForm, name: e.target.value })} - /> - - setCreateForm({ ...createForm, dosage: e.target.value })} - /> - setCreateForm({ ...createForm, frequency: e.target.value })} - /> - - - setCreateForm({ ...createForm, route: e.target.value as CreateMedicationRequest['route'] }) - } - > - {ROUTES.map((r) => ( - - {r} - - ))} - - - setCreateForm({ ...createForm, instructions: e.target.value || undefined }) - } - /> - - - - - - - - - {/* Edit dialog */} - setEditTarget(null)} - fullWidth - maxWidth="sm" - > - Edit medication - - - setEditForm({ ...editForm, name: e.target.value })} - /> - - setEditForm({ ...editForm, dosage: e.target.value })} - /> - setEditForm({ ...editForm, frequency: e.target.value })} - /> - - setEditForm({ ...editForm, instructions: e.target.value })} - /> - setEditForm({ ...editForm, active: e.target.value === 'active' })} - > - active - inactive - - - - - - - - - - {/* Delete confirm */} - setDeleteTarget(null)}> - Delete medication? - - - Remove {deleteTarget?.name}? This cannot be undone. - - - - - - - - - ); -}; - -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;