normogen/web/normogen-web/src/components/interactions/InteractionsChecker.tsx
goose 1515923996 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).
2026-06-28 04:31:16 -03:00

130 lines
4.3 KiB
TypeScript

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<string[]>([]);
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 (
<Box>
<Typography variant="h6" sx={{ mb: 1 }}>
Drug interactions
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Select two or more of your medications to check for known interactions.
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }} onClose={clearError}>
{error}
</Alert>
)}
{medications.length === 0 ? (
<Typography color="text.secondary" sx={{ py: 4, textAlign: 'center' }}>
Add medications first (Medications tab), then check them for interactions.
</Typography>
) : (
<>
<Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', gap: 1, mb: 2 }}>
{medications.map((m) => {
const on = selected.includes(m.name);
return (
<Chip
key={m.medication_id}
label={m.name}
color={on ? 'primary' : 'default'}
variant={on ? 'filled' : 'outlined'}
onClick={() => toggle(m.name)}
/>
);
})}
</Stack>
<Button
variant="contained"
onClick={runCheck}
disabled={selected.length < 2 || isChecking}
sx={{ mb: 3 }}
>
{isChecking ? <CircularProgress size={24} /> : 'Check interactions'}
</Button>
{/* Results */}
{hasChecked && !isChecking && selected.length >= 2 && (
<>
{interactions.length === 0 ? (
<Alert icon={<CheckCircleIcon fontSize="inherit" />} severity="success">
No known interactions found between the selected medications.
</Alert>
) : (
<Stack spacing={2}>
<Alert icon={<WarningAmberIcon fontSize="inherit" />} severity="warning">
{interactions.length} interaction{interactions.length > 1 ? 's' : ''} found.
</Alert>
{interactions.map((ix, i) => (
<Card key={i} variant="outlined">
<CardContent>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ mb: 1 }}
>
<Typography variant="subtitle1" fontWeight={600}>
{ix.medications.join(' + ')}
</Typography>
<SeverityChip severity={ix.severity} />
</Stack>
<Typography variant="body2" sx={{ mb: 1 }}>
{ix.description}
</Typography>
{ix.disclaimer && (
<Typography variant="caption" color="text.secondary" fontStyle="italic">
{ix.disclaimer}
</Typography>
)}
</CardContent>
</Card>
))}
</Stack>
)}
</>
)}
</>
)}
</Box>
);
};
export default InteractionsChecker;