Files
maternal-app/maternal-web/app/analytics/page.tsx
Andrei 8276db39a2
Some checks failed
CI/CD Pipeline / Build Application (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
Add skeleton loading states across all tracking pages
- Replace CircularProgress spinners with content-aware skeleton screens
- Add FormSkeleton for form loading states (feeding, sleep, diaper pages)
- Add ActivityListSkeleton for recent activities loading
- Improves perceived performance with layout-matching placeholders

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-01 20:36:11 +00:00

272 lines
8.6 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 { ErrorBoundary } from '@/components/common/ErrorBoundary';
import { ChartErrorFallback } from '@/components/common/ErrorFallbacks';
import { StatGridSkeleton, ChartSkeleton } from '@/components/common/LoadingSkeletons';
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>
<Typography variant="h4" gutterBottom fontWeight="600" sx={{ mb: 3 }}>
Analytics & Insights
</Typography>
<StatGridSkeleton count={3} />
<Box sx={{ mt: 4 }}>
<ChartSkeleton height={350} />
</Box>
</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}>
<ErrorBoundary isolate fallback={<ChartErrorFallback />}>
<Box sx={{ p: 3 }}>
<WeeklySleepChart />
</Box>
</ErrorBoundary>
</TabPanel>
<TabPanel value={tabValue} index={1}>
<ErrorBoundary isolate fallback={<ChartErrorFallback />}>
<Box sx={{ p: 3 }}>
<FeedingFrequencyGraph />
</Box>
</ErrorBoundary>
</TabPanel>
<TabPanel value={tabValue} index={2}>
<ErrorBoundary isolate fallback={<ChartErrorFallback />}>
<Box sx={{ p: 3 }}>
<GrowthCurve />
</Box>
</ErrorBoundary>
</TabPanel>
<TabPanel value={tabValue} index={3}>
<Box sx={{ p: 3 }}>
<PatternInsights insights={insights} />
</Box>
</TabPanel>
</Paper>
</motion.div>
</Box>
</AppShell>
</ProtectedRoute>
);
}