Add Phase 4, 5 & 6: AI Assistant, Analytics & Testing
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>
This commit is contained in:
274
maternal-web/components/analytics/FeedingFrequencyGraph.tsx
Normal file
274
maternal-web/components/analytics/FeedingFrequencyGraph.tsx
Normal file
@@ -0,0 +1,274 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Typography, CircularProgress, Alert, ToggleButtonGroup, ToggleButton } from '@mui/material';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
} from 'recharts';
|
||||
import { format, subDays } from 'date-fns';
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
interface FeedingData {
|
||||
date: string;
|
||||
breastfeeding: number;
|
||||
bottle: number;
|
||||
solids: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface FeedingTypeData {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
breastfeeding: '#FFB6C1',
|
||||
bottle: '#FFA5B0',
|
||||
solids: '#FF94A5',
|
||||
};
|
||||
|
||||
export default function FeedingFrequencyGraph() {
|
||||
const [data, setData] = useState<FeedingData[]>([]);
|
||||
const [typeData, setTypeData] = useState<FeedingTypeData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [chartType, setChartType] = useState<'bar' | 'line'>('bar');
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeedingData();
|
||||
}, []);
|
||||
|
||||
const fetchFeedingData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const endDate = new Date();
|
||||
const startDate = subDays(endDate, 6);
|
||||
|
||||
const response = await apiClient.get('/api/v1/activities/feeding', {
|
||||
params: {
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
const feedingActivities = response.data.data;
|
||||
const dailyData: { [key: string]: FeedingData } = {};
|
||||
|
||||
// Initialize 7 days of data
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = subDays(endDate, 6 - i);
|
||||
const dateStr = format(date, 'MMM dd');
|
||||
dailyData[dateStr] = {
|
||||
date: dateStr,
|
||||
breastfeeding: 0,
|
||||
bottle: 0,
|
||||
solids: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Count feeding types by day
|
||||
const typeCounts = {
|
||||
breastfeeding: 0,
|
||||
bottle: 0,
|
||||
solids: 0,
|
||||
};
|
||||
|
||||
feedingActivities.forEach((activity: any) => {
|
||||
const dateStr = format(new Date(activity.timestamp), 'MMM dd');
|
||||
if (dailyData[dateStr]) {
|
||||
const type = activity.type?.toLowerCase() || 'bottle';
|
||||
|
||||
if (type === 'breastfeeding' || type === 'breast') {
|
||||
dailyData[dateStr].breastfeeding += 1;
|
||||
typeCounts.breastfeeding += 1;
|
||||
} else if (type === 'bottle' || type === 'formula') {
|
||||
dailyData[dateStr].bottle += 1;
|
||||
typeCounts.bottle += 1;
|
||||
} else if (type === 'solids' || type === 'solid') {
|
||||
dailyData[dateStr].solids += 1;
|
||||
typeCounts.solids += 1;
|
||||
}
|
||||
|
||||
dailyData[dateStr].total += 1;
|
||||
}
|
||||
});
|
||||
|
||||
setData(Object.values(dailyData));
|
||||
|
||||
// Prepare pie chart data
|
||||
const pieData: FeedingTypeData[] = [
|
||||
{
|
||||
name: 'Breastfeeding',
|
||||
value: typeCounts.breastfeeding,
|
||||
color: COLORS.breastfeeding,
|
||||
},
|
||||
{
|
||||
name: 'Bottle',
|
||||
value: typeCounts.bottle,
|
||||
color: COLORS.bottle,
|
||||
},
|
||||
{
|
||||
name: 'Solids',
|
||||
value: typeCounts.solids,
|
||||
color: COLORS.solids,
|
||||
},
|
||||
].filter((item) => item.value > 0);
|
||||
|
||||
setTypeData(pieData);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch feeding data:', err);
|
||||
setError(err.response?.data?.message || 'Failed to load feeding data');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChartTypeChange = (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
newType: 'bar' | 'line' | null
|
||||
) => {
|
||||
if (newType !== null) {
|
||||
setChartType(newType);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" sx={{ borderRadius: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom fontWeight="600">
|
||||
Weekly Feeding Patterns
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Track feeding frequency and types over the past 7 days
|
||||
</Typography>
|
||||
</Box>
|
||||
<ToggleButtonGroup
|
||||
value={chartType}
|
||||
exclusive
|
||||
onChange={handleChartTypeChange}
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="bar">Bar</ToggleButton>
|
||||
<ToggleButton value="line">Line</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
|
||||
{/* Feeding Frequency Chart */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="subtitle1" gutterBottom fontWeight="600">
|
||||
Daily Feeding Frequency by Type
|
||||
</Typography>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
{chartType === 'bar' ? (
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||
<XAxis dataKey="date" stroke="#666" />
|
||||
<YAxis stroke="#666" label={{ value: 'Count', angle: -90, position: 'insideLeft' }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar dataKey="breastfeeding" stackId="a" fill={COLORS.breastfeeding} name="Breastfeeding" />
|
||||
<Bar dataKey="bottle" stackId="a" fill={COLORS.bottle} name="Bottle" />
|
||||
<Bar dataKey="solids" stackId="a" fill={COLORS.solids} name="Solids" />
|
||||
</BarChart>
|
||||
) : (
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||
<XAxis dataKey="date" stroke="#666" />
|
||||
<YAxis stroke="#666" label={{ value: 'Count', angle: -90, position: 'insideLeft' }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="total"
|
||||
stroke={COLORS.breastfeeding}
|
||||
strokeWidth={3}
|
||||
dot={{ fill: COLORS.breastfeeding, r: 5 }}
|
||||
name="Total Feedings"
|
||||
/>
|
||||
</LineChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
|
||||
{/* Feeding Type Distribution */}
|
||||
{typeData.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom fontWeight="600">
|
||||
Feeding Type Distribution (7 days)
|
||||
</Typography>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={typeData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
|
||||
outerRadius={100}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{typeData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user