- Created EnhancedInsightsDashboard with multiple chart types: - Area charts with gradients for activity trends - Radar chart for weekly activity patterns - 24-hour heatmap visualization - Bubble/scatter chart for correlations - Time of day distribution bar chart - Added toggle between basic and enhanced chart views - Implemented chart export functionality (PNG/PDF) - Fixed API endpoint URLs (circadian-rhythm, query params) - Fixed component library conflicts (shadcn/ui → MUI) - Added comprehensive null safety for timestamp handling - Added alert type translations in all 5 languages - Installed html2canvas and jspdf for export features - Applied consistent minimum width styling to all charts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
291 lines
9.4 KiB
TypeScript
291 lines
9.4 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 { EnhancedInsightsDashboard } from './EnhancedInsightsDashboard';
|
|
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';
|
|
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
|
import { ShowChart, BubbleChart } from '@mui/icons-material';
|
|
|
|
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>('');
|
|
const [chartMode, setChartMode] = useState<'basic' | 'enhanced'>('basic');
|
|
|
|
// 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>
|
|
|
|
{/* Chart Mode Toggle for Insights Tab */}
|
|
{tabValue === 0 && (
|
|
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'center' }}>
|
|
<ToggleButtonGroup
|
|
value={chartMode}
|
|
exclusive
|
|
onChange={(e, newMode) => newMode && setChartMode(newMode)}
|
|
size="small"
|
|
>
|
|
<ToggleButton value="basic" aria-label="basic charts">
|
|
<ShowChart sx={{ mr: 1 }} />
|
|
Basic Charts
|
|
</ToggleButton>
|
|
<ToggleButton value="enhanced" aria-label="enhanced charts">
|
|
<BubbleChart sx={{ mr: 1 }} />
|
|
Enhanced Charts
|
|
</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
</Box>
|
|
)}
|
|
|
|
{/* Tab Panels */}
|
|
<TabPanel value={tabValue} index={0}>
|
|
{/* Insights tab shows either basic or enhanced dashboard */}
|
|
{chartMode === 'basic' ? (
|
|
<InsightsDashboard selectedChildId={selectedChildId} days={days} />
|
|
) : (
|
|
<EnhancedInsightsDashboard 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>
|
|
);
|
|
}
|