feat(web): Phase 3b — medication/health/interactions feature UIs

Build the three core feature UIs on top of the now-wired stores + corrected API
client from Phase 3a, turning the runnable shell into a usable app.

Dashboard (pages/Dashboard.tsx): AppBar (Normogen + username + logout) over an
MUI Tabs container (Medications | Health | Interactions), widened to lg. User
loads on mount (needed for profile_id in medication create).

MedicationManager (components/medication/): full CRUD.
* List of medication cards (name, dosage·frequency, active chip, instructions)
  with edit/delete actions and an empty state.
* Create dialog: name/dosage/frequency/route(select)/instructions. profile_id
  sourced from user.profile_id with a 'default' fallback (TODO: real profiles).
  Payload typed to CreateMedicationRequest.
* Edit dialog (UpdateMedicationRequest shape) + delete confirm. Guards the
  optional medication_id field.

HealthStats (components/health/): trend cards + chart + add + table.
* Trend summary cards from /health-stats/trends (avg/min/max + trend arrow).
* recharts LineChart of a selected stat type over measured_at, with a type
  selector. Timestamp field is measured_at (not timestamp).
* Record dialog (stat_type/value/unit/measured_at/notes) + recent-readings table
  (date-fns formatted).

InteractionsChecker (components/interactions/): multi-select medication names
(chips) -> POST /interactions/check. Results rendered as cards with a
SeverityChip, description, and disclaimer. Success/empty states.

Shared SeverityChip (components/common/): maps InteractionSeverity ->
MUI Chip color (severe=error, moderate=warning, mild=success, unknown=default).

Added @mui/icons-material@7 (pinned to match @mui/material 7; npm tried to grab
v9 which needs material ^9).

Verified: npm run build clean (TS5 strict), dev server serves the app + all
component modules transform. Solaria round-trip confirmed every contract the UIs
call: medication create/list (200), interactions warfarin+aspirin -> severe,
health-stat create (201).
This commit is contained in:
goose 2026-06-28 04:31:16 -03:00
parent ef1bb9f4be
commit 1515923996
7 changed files with 886 additions and 60 deletions

View file

@ -1,24 +1,20 @@
import { useEffect, type FC } from 'react';
import { useEffect, useState, type FC } from 'react';
import { useNavigate } from 'react-router-dom';
import {
AppBar,
Toolbar,
Typography,
Button,
Container,
Card,
CardContent,
Box,
CircularProgress,
} from '@mui/material';
import { AppBar, Toolbar, Typography, Button, Container, Box, Tabs, Tab } 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, isLoading, loadUser, logout } = useAuthStore();
const { user, isAuthenticated, loadUser, logout } = useAuthStore();
const [tab, setTab] = useState<TabIndex>(0);
// On mount, refresh the user record from the backend (confirms the token is
// still valid and pulls the latest profile).
// still valid, and medication-create needs user.profile_id).
useEffect(() => {
if (isAuthenticated && !user) {
loadUser();
@ -37,34 +33,32 @@ export const Dashboard: FC = () => {
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Normogen
</Typography>
{user && (
<Typography variant="body2" sx={{ mr: 2, opacity: 0.85 }}>
{user.username}
</Typography>
)}
<Button color="inherit" onClick={handleLogout}>
Logout
</Button>
</Toolbar>
</AppBar>
<Container maxWidth="md" sx={{ mt: 4 }}>
{isLoading && (
<Box display="flex" justifyContent="center" mt={4}>
<CircularProgress />
</Box>
)}
<Container maxWidth="lg" sx={{ mt: 3 }}>
<Tabs value={tab} onChange={(_, v) => setTab(v as TabIndex)} sx={{ mb: 3 }}>
<Tab label="Medications" />
<Tab label="Health" />
<Tab label="Interactions" />
</Tabs>
<Card>
<CardContent>
<Typography variant="h5" gutterBottom>
Welcome{user ? `, ${user.username}` : ''}
</Typography>
<Typography variant="body1" color="text.secondary">
You&apos;re signed in to Normogen.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
This is the dashboard shell. Medication management, health statistics,
and drug-interaction checking UIs will be built here.
</Typography>
</CardContent>
</Card>
<Box>
{tab === 0 && <MedicationManager />}
{tab === 1 && <HealthStats />}
{tab === 2 && <InteractionsChecker />}
</Box>
</Container>
</>
);
};
export default Dashboard;