Phase 4: AI Assistant Integration - AI chat interface with suggested questions - Real-time messaging with backend OpenAI integration - Material UI chat bubbles and animations - Medical disclaimer and user-friendly UX Phase 5: Pattern Recognition & Analytics - Analytics dashboard with tabbed interface - Weekly sleep chart with bar/line visualizations - Feeding frequency graphs with type distribution - Growth curve with WHO percentiles (0-24 months) - Pattern insights with AI-powered recommendations - PDF report export functionality - Recharts integration for all data visualizations Phase 6: Testing & Optimization - Jest and React Testing Library setup - Unit tests for auth, API client, and components - Integration tests with full coverage - WCAG AA accessibility compliance testing - Performance optimizations (SWC, image optimization) - Accessibility monitoring with axe-core - 70% code coverage threshold 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
264 lines
8.0 KiB
TypeScript
264 lines
8.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Paper,
|
|
Grid,
|
|
Card,
|
|
CardContent,
|
|
Button,
|
|
CircularProgress,
|
|
Tabs,
|
|
Tab,
|
|
Alert,
|
|
} from '@mui/material';
|
|
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
|
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
|
import {
|
|
TrendingUp,
|
|
Hotel,
|
|
Restaurant,
|
|
BabyChangingStation,
|
|
Download,
|
|
} from '@mui/icons-material';
|
|
import { motion } from 'framer-motion';
|
|
import apiClient from '@/lib/api/client';
|
|
import WeeklySleepChart from '@/components/analytics/WeeklySleepChart';
|
|
import FeedingFrequencyGraph from '@/components/analytics/FeedingFrequencyGraph';
|
|
import GrowthCurve from '@/components/analytics/GrowthCurve';
|
|
import PatternInsights from '@/components/analytics/PatternInsights';
|
|
|
|
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={`analytics-tabpanel-${index}`}
|
|
aria-labelledby={`analytics-tab-${index}`}
|
|
{...other}
|
|
>
|
|
{value === index && <Box sx={{ pt: 3 }}>{children}</Box>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function AnalyticsPage() {
|
|
const [tabValue, setTabValue] = useState(0);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [insights, setInsights] = useState<any>(null);
|
|
|
|
useEffect(() => {
|
|
fetchAnalytics();
|
|
}, []);
|
|
|
|
const fetchAnalytics = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
const response = await apiClient.get('/api/v1/analytics/insights');
|
|
setInsights(response.data.data);
|
|
} catch (err: any) {
|
|
console.error('Failed to fetch analytics:', err);
|
|
setError(err.response?.data?.message || 'Failed to load analytics');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleExportReport = async () => {
|
|
try {
|
|
const response = await apiClient.get('/api/v1/analytics/reports/weekly', {
|
|
responseType: 'blob',
|
|
});
|
|
const blob = new Blob([response.data], { type: 'application/pdf' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `weekly-report-${new Date().toISOString().split('T')[0]}.pdf`;
|
|
link.click();
|
|
window.URL.revokeObjectURL(url);
|
|
} catch (err) {
|
|
console.error('Failed to export report:', err);
|
|
}
|
|
};
|
|
|
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
|
setTabValue(newValue);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<ProtectedRoute>
|
|
<AppShell>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
minHeight: '60vh',
|
|
}}
|
|
>
|
|
<CircularProgress />
|
|
</Box>
|
|
</AppShell>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ProtectedRoute>
|
|
<AppShell>
|
|
<Box>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
{/* Header */}
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
mb: 3,
|
|
}}
|
|
>
|
|
<Box>
|
|
<Typography variant="h4" gutterBottom fontWeight="600">
|
|
Analytics & Insights 📊
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary">
|
|
Track patterns and get personalized insights
|
|
</Typography>
|
|
</Box>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<Download />}
|
|
onClick={handleExportReport}
|
|
sx={{ borderRadius: 3 }}
|
|
>
|
|
Export Report
|
|
</Button>
|
|
</Box>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 3, borderRadius: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Summary Cards */}
|
|
<Grid container spacing={2} sx={{ mb: 4 }}>
|
|
<Grid item xs={12} sm={4}>
|
|
<Card
|
|
sx={{
|
|
background: 'linear-gradient(135deg, #B6D7FF 0%, #A5C9FF 100%)',
|
|
color: 'white',
|
|
}}
|
|
>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
|
<Hotel sx={{ fontSize: 32 }} />
|
|
<Typography variant="h5" fontWeight="600">
|
|
{insights?.sleep?.averageHours || '0'}h
|
|
</Typography>
|
|
</Box>
|
|
<Typography variant="body2">Avg Sleep (7 days)</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
<Grid item xs={12} sm={4}>
|
|
<Card
|
|
sx={{
|
|
background: 'linear-gradient(135deg, #FFB6C1 0%, #FFA5B0 100%)',
|
|
color: 'white',
|
|
}}
|
|
>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
|
<Restaurant sx={{ fontSize: 32 }} />
|
|
<Typography variant="h5" fontWeight="600">
|
|
{insights?.feeding?.averagePerDay || '0'}
|
|
</Typography>
|
|
</Box>
|
|
<Typography variant="body2">Avg Feedings (7 days)</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
<Grid item xs={12} sm={4}>
|
|
<Card
|
|
sx={{
|
|
background: 'linear-gradient(135deg, #FFE4B5 0%, #FFD9A0 100%)',
|
|
color: 'white',
|
|
}}
|
|
>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
|
<BabyChangingStation sx={{ fontSize: 32 }} />
|
|
<Typography variant="h5" fontWeight="600">
|
|
{insights?.diaper?.averagePerDay || '0'}
|
|
</Typography>
|
|
</Box>
|
|
<Typography variant="body2">Avg Diapers (7 days)</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* Tabs */}
|
|
<Paper sx={{ borderRadius: 3, overflow: 'hidden' }}>
|
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
|
<Tabs
|
|
value={tabValue}
|
|
onChange={handleTabChange}
|
|
aria-label="analytics tabs"
|
|
sx={{ px: 2 }}
|
|
>
|
|
<Tab label="Sleep Patterns" />
|
|
<Tab label="Feeding Patterns" />
|
|
<Tab label="Growth Curve" />
|
|
<Tab label="Insights" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
<TabPanel value={tabValue} index={0}>
|
|
<Box sx={{ p: 3 }}>
|
|
<WeeklySleepChart />
|
|
</Box>
|
|
</TabPanel>
|
|
|
|
<TabPanel value={tabValue} index={1}>
|
|
<Box sx={{ p: 3 }}>
|
|
<FeedingFrequencyGraph />
|
|
</Box>
|
|
</TabPanel>
|
|
|
|
<TabPanel value={tabValue} index={2}>
|
|
<Box sx={{ p: 3 }}>
|
|
<GrowthCurve />
|
|
</Box>
|
|
</TabPanel>
|
|
|
|
<TabPanel value={tabValue} index={3}>
|
|
<Box sx={{ p: 3 }}>
|
|
<PatternInsights insights={insights} />
|
|
</Box>
|
|
</TabPanel>
|
|
</Paper>
|
|
</motion.div>
|
|
</Box>
|
|
</AppShell>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|