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).
130 lines
4.3 KiB
TypeScript
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;
|