feat: Add advanced analytics UI components in frontend
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

- 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.
This commit is contained in:
2025-10-06 11:46:05 +00:00
parent 56d2d83418
commit b0ac2f71df
19 changed files with 3112 additions and 13 deletions

View File

@@ -0,0 +1,365 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useAuth } from '@/lib/auth/AuthContext';
import { childrenApi, Child } from '@/lib/api/children';
import {
analyticsApi,
CircadianRhythm,
AnomalyDetection,
GrowthAnalysis,
CorrelationAnalysis,
TrendAnalysis,
} from '@/lib/api/analytics';
import { CircadianRhythmCard } from '@/components/analytics/CircadianRhythmCard';
import { AnomalyAlertsPanel } from '@/components/analytics/AnomalyAlertsPanel';
import { GrowthPercentileChart } from '@/components/analytics/GrowthPercentileChart';
import { CorrelationInsights } from '@/components/analytics/CorrelationInsights';
import { TrendAnalysisChart } from '@/components/analytics/TrendAnalysisChart';
import {
Card,
CardContent,
CardHeader,
Button,
Alert,
AlertTitle,
Tabs,
Tab,
Box,
Select,
MenuItem,
FormControl,
InputLabel,
} from '@mui/material';
import { Loader2, RefreshCw, Activity, Brain, TrendingUp, Baby, Link } from 'lucide-react';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
export default function AdvancedAnalyticsPage() {
const { user } = useAuth();
const [children, setChildren] = useState<Child[]>([]);
const [selectedChildId, setSelectedChildId] = useState<string>('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
// Analytics data states
const [circadianData, setCircadianData] = useState<CircadianRhythm | null>(null);
const [anomalyData, setAnomalyData] = useState<AnomalyDetection | null>(null);
const [growthData, setGrowthData] = useState<GrowthAnalysis | null>(null);
const [correlationData, setCorrelationData] = useState<CorrelationAnalysis | null>(null);
const [sleepTrendData, setSleepTrendData] = useState<TrendAnalysis | null>(null);
const [feedingTrendData, setFeedingTrendData] = useState<TrendAnalysis | null>(null);
// Loading states for each component
const [circadianLoading, setCircadianLoading] = useState(false);
const [anomalyLoading, setAnomalyLoading] = useState(false);
const [growthLoading, setGrowthLoading] = useState(false);
const [correlationLoading, setCorrelationLoading] = useState(false);
const [trendLoading, setTrendLoading] = useState(false);
// Error states for each component
const [circadianError, setCircadianError] = useState<Error | null>(null);
const [anomalyError, setAnomalyError] = useState<Error | null>(null);
const [growthError, setGrowthError] = useState<Error | null>(null);
const [correlationError, setCorrelationError] = useState<Error | null>(null);
const [trendError, setTrendError] = useState<Error | null>(null);
const familyId = user?.families?.[0]?.familyId;
useEffect(() => {
if (familyId) {
loadChildren();
}
}, [familyId]);
useEffect(() => {
if (selectedChildId && children.length > 0) {
const childExists = children.some(child => child.id === selectedChildId);
if (childExists) {
loadAllAnalytics();
} else {
console.warn('[AdvancedAnalytics] Selected child not found, resetting');
setSelectedChildId(children[0].id);
}
}
}, [selectedChildId, children]);
const loadChildren = async () => {
if (!familyId) {
setLoading(false);
setError('No family found');
return;
}
try {
console.log('[AdvancedAnalytics] Loading children for familyId:', familyId);
const data = await childrenApi.getChildren(familyId);
console.log('[AdvancedAnalytics] Loaded children:', data);
setChildren(data);
if (data.length > 0) {
const existingChildStillValid = data.some(child => child.id === selectedChildId);
if (!selectedChildId || !existingChildStillValid) {
setSelectedChildId(data[0].id);
}
}
setError('');
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load children:', error);
setError('Failed to load children');
} finally {
setLoading(false);
}
};
const loadAllAnalytics = () => {
loadCircadianRhythm();
loadAnomalies();
loadGrowthAnalysis();
loadCorrelations();
loadTrends();
};
const loadCircadianRhythm = async () => {
if (!selectedChildId) return;
setCircadianLoading(true);
setCircadianError(null);
try {
const data = await analyticsApi.getCircadianRhythm(selectedChildId, 14);
setCircadianData(data);
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load circadian rhythm:', error);
setCircadianError(error as Error);
} finally {
setCircadianLoading(false);
}
};
const loadAnomalies = async () => {
if (!selectedChildId) return;
setAnomalyLoading(true);
setAnomalyError(null);
try {
const data = await analyticsApi.getAnomalies(selectedChildId, 30);
setAnomalyData(data);
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load anomalies:', error);
setAnomalyError(error as Error);
} finally {
setAnomalyLoading(false);
}
};
const loadGrowthAnalysis = async () => {
if (!selectedChildId) return;
setGrowthLoading(true);
setGrowthError(null);
try {
const data = await analyticsApi.getGrowthAnalysis(selectedChildId);
setGrowthData(data);
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load growth analysis:', error);
setGrowthError(error as Error);
} finally {
setGrowthLoading(false);
}
};
const loadCorrelations = async () => {
if (!selectedChildId) return;
setCorrelationLoading(true);
setCorrelationError(null);
try {
const data = await analyticsApi.getCorrelations(selectedChildId, 14);
setCorrelationData(data);
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load correlations:', error);
setCorrelationError(error as Error);
} finally {
setCorrelationLoading(false);
}
};
const loadTrends = async () => {
if (!selectedChildId) return;
setTrendLoading(true);
setTrendError(null);
try {
const [sleepTrend, feedingTrend] = await Promise.all([
analyticsApi.getTrends(selectedChildId, 'sleep'),
analyticsApi.getTrends(selectedChildId, 'feeding'),
]);
setSleepTrendData(sleepTrend);
setFeedingTrendData(feedingTrend);
} catch (error) {
console.error('[AdvancedAnalytics] Failed to load trends:', error);
setTrendError(error as Error);
} finally {
setTrendLoading(false);
}
};
if (loading) {
return (
<ProtectedRoute>
<AppShell>
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
</AppShell>
</ProtectedRoute>
);
}
if (children.length === 0) {
return (
<ProtectedRoute>
<AppShell>
<div className="p-6">
<Alert>
<AlertTitle>No Children Found</AlertTitle>
<AlertDescription>
Add a child to your family to view advanced analytics.
</AlertDescription>
</Alert>
</div>
</AppShell>
</ProtectedRoute>
);
}
return (
<ProtectedRoute>
<AppShell>
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Advanced Analytics</h1>
<p className="text-gray-600 mt-1">
AI-powered insights and deep analysis of your child's patterns
</p>
</div>
<Button onClick={loadAllAnalytics} variant="outline">
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
{/* Child Selector */}
<Card>
<CardContent className="py-4">
<div className="flex items-center gap-4">
<label htmlFor="child-select" className="text-sm font-medium">
Select Child:
</label>
<Select
value={selectedChildId}
onValueChange={setSelectedChildId}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select a child" />
</SelectTrigger>
<SelectContent>
{children.map((child) => (
<SelectItem key={child.id} value={child.id}>
{child.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{error && (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Analytics Tabs */}
<Tabs defaultValue="circadian" className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="circadian">
<Brain className="h-4 w-4 mr-2" />
Sleep Rhythm
</TabsTrigger>
<TabsTrigger value="anomalies">
<Activity className="h-4 w-4 mr-2" />
Anomalies
</TabsTrigger>
<TabsTrigger value="growth">
<Baby className="h-4 w-4 mr-2" />
Growth
</TabsTrigger>
<TabsTrigger value="correlations">
<Link className="h-4 w-4 mr-2" />
Correlations
</TabsTrigger>
<TabsTrigger value="trends">
<TrendingUp className="h-4 w-4 mr-2" />
Trends
</TabsTrigger>
</TabsList>
<TabsContent value="circadian" className="space-y-4">
<CircadianRhythmCard
data={circadianData}
loading={circadianLoading}
error={circadianError}
/>
</TabsContent>
<TabsContent value="anomalies" className="space-y-4">
<AnomalyAlertsPanel
data={anomalyData}
loading={anomalyLoading}
error={anomalyError}
/>
</TabsContent>
<TabsContent value="growth" className="space-y-4">
<GrowthPercentileChart
data={growthData}
loading={growthLoading}
error={growthError}
/>
</TabsContent>
<TabsContent value="correlations" className="space-y-4">
<CorrelationInsights
data={correlationData}
loading={correlationLoading}
error={correlationError}
/>
</TabsContent>
<TabsContent value="trends" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<TrendAnalysisChart
data={sleepTrendData}
activityType="Sleep"
loading={trendLoading}
error={trendError}
/>
<TrendAnalysisChart
data={feedingTrendData}
activityType="Feeding"
loading={trendLoading}
error={trendError}
/>
</div>
</TabsContent>
</Tabs>
</div>
</AppShell>
</ProtectedRoute>
);
}