Files
maternal-app/maternal-web/components/features/analytics/UnifiedInsightsDashboard.tsx
Andrei b0ac2f71df
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
feat: Add advanced analytics UI components in frontend
- Add comprehensive API client methods for all advanced analytics endpoints
- Create CircadianRhythmCard component for sleep pattern visualization
- Create AnomalyAlertsPanel for anomaly detection and alerts
- Create GrowthPercentileChart with WHO/CDC percentiles
- Create CorrelationInsights for activity correlations
- Create TrendAnalysisChart with predictions
- Add advanced analytics page with all new components
- Add UI component library (shadcn/ui) setup
- Add navigation link to advanced analytics from insights page

All advanced analytics features are now accessible from the frontend UI.
2025-10-06 11:46:05 +00:00

262 lines
8.2 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Tabs,
Tab,
Select,
MenuItem,
FormControl,
InputLabel,
CircularProgress,
Alert,
Grid,
IconButton,
Button,
} from '@mui/material';
import { Timeline, TrendingUp, Assessment, ArrowBack } from '@mui/icons-material';
import { useRouter } from 'next/navigation';
import { childrenApi, Child } from '@/lib/api/children';
import { analyticsApi, PatternInsights, PredictionInsights } from '@/lib/api/analytics';
import { InsightsDashboard } from './InsightsDashboard';
import PredictionsCard from './PredictionsCard';
import GrowthSpurtAlert from './GrowthSpurtAlert';
import ChildSelector from '@/components/common/ChildSelector';
import { motion } from 'framer-motion';
import { useAuth } from '@/lib/auth/AuthContext';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`insights-tabpanel-${index}`}
aria-labelledby={`insights-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ pt: 3 }}>{children}</Box>}
</div>
);
}
export function UnifiedInsightsDashboard() {
const router = useRouter();
const { user } = useAuth();
const [children, setChildren] = useState<Child[]>([]);
const [selectedChildIds, setSelectedChildIds] = useState<string[]>([]);
const [tabValue, setTabValue] = useState(0);
const [loading, setLoading] = useState(true);
const [insights, setInsights] = useState<PatternInsights | null>(null);
const [predictions, setPredictions] = useState<PredictionInsights | null>(null);
const [insightsLoading, setInsightsLoading] = useState(false);
const [predictionsLoading, setPredictionsLoading] = useState(false);
const [days, setDays] = useState<number>(7);
const [error, setError] = useState<string>('');
// Get the selected child ID (first one from the array for single selection)
const selectedChildId = selectedChildIds[0] || '';
const familyId = user?.families?.[0]?.familyId;
useEffect(() => {
if (familyId) {
loadChildren();
}
}, [familyId]);
useEffect(() => {
if (selectedChildId && children.length > 0) {
// Validate that selectedChildId belongs to current user's children
const childExists = children.some(child => child.id === selectedChildId);
if (childExists) {
loadInsights();
loadPredictions();
} else {
// Invalid child ID - reset to first child
console.warn('[UnifiedInsightsDashboard] Selected child not found in user\'s children, resetting');
setSelectedChildIds([children[0].id]);
setError('Selected child not found. Showing data for your first child.');
}
}
}, [selectedChildId, days, children]);
const loadChildren = async () => {
if (!familyId) {
setLoading(false);
setError('No family found');
return;
}
try {
console.log('[UnifiedInsightsDashboard] Loading children for familyId:', familyId);
const data = await childrenApi.getChildren(familyId);
console.log('[UnifiedInsightsDashboard] Loaded children:', data);
setChildren(data);
// Only set selectedChildIds if we don't have one or if it's not in the new list
if (data.length > 0) {
const existingChildStillValid = data.some(child => child.id === selectedChildId);
if (!selectedChildId || !existingChildStillValid) {
setSelectedChildIds([data[0].id]);
}
}
setError('');
} catch (error) {
console.error('[UnifiedInsightsDashboard] Failed to load children:', error);
setError('Failed to load children');
} finally {
setLoading(false);
}
};
const loadInsights = async () => {
if (!selectedChildId) return;
setInsightsLoading(true);
try {
const data = await analyticsApi.getInsights(selectedChildId, days);
setInsights(data);
} catch (error) {
console.error('Failed to load insights:', error);
} finally {
setInsightsLoading(false);
}
};
const loadPredictions = async () => {
if (!selectedChildId) return;
setPredictionsLoading(true);
try {
const data = await analyticsApi.getPredictions(selectedChildId);
setPredictions(data);
} catch (error) {
console.error('Failed to load predictions:', error);
} finally {
setPredictionsLoading(false);
}
};
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
<CircularProgress />
</Box>
);
}
if (children.length === 0) {
return (
<Box sx={{ p: 3 }}>
<Alert severity="info">Add a child to your family to view insights and predictions.</Alert>
</Box>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Box sx={{ px: { xs: 2, sm: 3 }, py: 3 }}>
{/* Header with Back Button */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<IconButton onClick={() => router.back()} sx={{ mr: 2 }}>
<ArrowBack />
</IconButton>
<Box>
<Typography variant="h4" fontWeight={600}>
Insights & Predictions
</Typography>
<Typography variant="body2" color="text.secondary">
AI-powered insights, patterns, and predictions for your child
</Typography>
</Box>
</Box>
<Button
variant="outlined"
color="primary"
onClick={() => router.push('/analytics/advanced')}
startIcon={<TrendingUp />}
>
Advanced Analytics
</Button>
</Box>
{/* Error Alert */}
{error && (
<Alert severity="warning" sx={{ mb: 3 }} onClose={() => setError('')}>
{error}
</Alert>
)}
{/* Shared Filters */}
<Box sx={{ mb: 3, display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'flex-start' }}>
{children.length > 1 && (
<Box sx={{ minWidth: 250 }}>
<ChildSelector
children={children}
selectedChildIds={selectedChildIds}
onChange={(childIds) => setSelectedChildIds(childIds)}
mode="single"
label="Child"
compact={false}
/>
</Box>
)}
<FormControl sx={{ minWidth: 150 }}>
<InputLabel>Time Period</InputLabel>
<Select
value={days}
label="Time Period"
onChange={(e) => setDays(Number(e.target.value))}
>
<MenuItem value={7}>Last 7 days</MenuItem>
<MenuItem value={30}>Last 30 days</MenuItem>
<MenuItem value={90}>Last 3 months</MenuItem>
</Select>
</FormControl>
</Box>
{/* Growth Spurt Alert */}
{insights?.growthSpurt && <GrowthSpurtAlert growthSpurt={insights.growthSpurt} />}
{/* Tabs */}
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)}>
<Tab label="Insights" icon={<Timeline />} iconPosition="start" />
<Tab label="Predictions" icon={<TrendingUp />} iconPosition="start" />
</Tabs>
</Box>
{/* Tab Panels */}
<TabPanel value={tabValue} index={0}>
{/* Insights tab shows the existing InsightsDashboard */}
<InsightsDashboard selectedChildId={selectedChildId} days={days} />
</TabPanel>
<TabPanel value={tabValue} index={1}>
{/* Predictions tab */}
<Grid container spacing={3}>
<Grid item xs={12}>
<PredictionsCard predictions={predictions} loading={predictionsLoading} />
</Grid>
</Grid>
</TabPanel>
</Box>
</motion.div>
);
}