Added detailed implementation plan covering: - Frontend: Dynamic UI, child selector, bulk activity logging, comparison analytics - Backend: Bulk operations, multi-child queries, family statistics - AI/Voice: Child name detection, context building, clarification flows - Database: Schema enhancements, user preferences, bulk operation tracking - State management, API enhancements, real-time sync updates - Testing strategy: Unit, integration, and E2E tests - Migration plan with feature flags for phased rollout - Performance optimizations: Caching, indexes, code splitting Also includes: - Security fixes for multi-family data leakage in analytics pages - ParentFlow branding updates - Activity tracking navigation improvements - Backend DTO and error handling fixes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
757 lines
28 KiB
TypeScript
757 lines
28 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Grid,
|
|
Card,
|
|
CardContent,
|
|
Select,
|
|
MenuItem,
|
|
FormControl,
|
|
InputLabel,
|
|
CircularProgress,
|
|
Alert,
|
|
Paper,
|
|
Divider,
|
|
List,
|
|
ListItem,
|
|
ListItemAvatar,
|
|
ListItemText,
|
|
Avatar,
|
|
Chip,
|
|
ToggleButtonGroup,
|
|
ToggleButton,
|
|
Button,
|
|
} from '@mui/material';
|
|
import {
|
|
Restaurant,
|
|
Hotel,
|
|
BabyChangingStation,
|
|
TrendingUp,
|
|
Timeline,
|
|
Assessment,
|
|
ChildCare,
|
|
Add,
|
|
} from '@mui/icons-material';
|
|
import { useRouter } from 'next/navigation';
|
|
import { motion } from 'framer-motion';
|
|
import { trackingApi, Activity, ActivityType } from '@/lib/api/tracking';
|
|
import { childrenApi, Child } from '@/lib/api/children';
|
|
import { subDays, startOfDay, endOfDay, parseISO, differenceInMinutes } from 'date-fns';
|
|
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
|
import { useFormatting } from '@/hooks/useFormatting';
|
|
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
|
import { useAuth } from '@/lib/auth/AuthContext';
|
|
|
|
type DateRange = '7days' | '30days' | '3months';
|
|
|
|
interface DayData {
|
|
date: string;
|
|
feedings: number;
|
|
sleepHours: number;
|
|
diapers: number;
|
|
activities: number;
|
|
}
|
|
|
|
interface DiaperTypeData {
|
|
name: string;
|
|
value: number;
|
|
color: string;
|
|
[key: string]: string | number;
|
|
}
|
|
|
|
interface ActivityTypeData {
|
|
name: string;
|
|
count: number;
|
|
color: string;
|
|
}
|
|
|
|
const COLORS = {
|
|
feeding: '#E91E63',
|
|
sleep: '#1976D2',
|
|
diaper: '#F57C00',
|
|
medication: '#C62828',
|
|
milestone: '#558B2F',
|
|
note: '#FFD3B6',
|
|
wet: '#87CEEB',
|
|
dirty: '#D2691E',
|
|
both: '#FF8C00',
|
|
dry: '#90EE90',
|
|
};
|
|
|
|
const getActivityIcon = (type: ActivityType) => {
|
|
switch (type) {
|
|
case 'feeding':
|
|
return <Restaurant />;
|
|
case 'sleep':
|
|
return <Hotel />;
|
|
case 'diaper':
|
|
return <BabyChangingStation />;
|
|
default:
|
|
return <Timeline />;
|
|
}
|
|
};
|
|
|
|
const getActivityColor = (type: ActivityType) => {
|
|
return COLORS[type as keyof typeof COLORS] || '#CCCCCC';
|
|
};
|
|
|
|
export const InsightsDashboard: React.FC = () => {
|
|
const router = useRouter();
|
|
const { user } = useAuth();
|
|
const { format, formatDistanceToNow } = useLocalizedDate();
|
|
const { t } = useTranslation('insights');
|
|
const { formatNumber } = useFormatting();
|
|
const [children, setChildren] = useState<Child[]>([]);
|
|
const [selectedChild, setSelectedChild] = useState<string>('');
|
|
const [dateRange, setDateRange] = useState<DateRange>('7days');
|
|
const [activities, setActivities] = useState<Activity[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const familyId = user?.families?.[0]?.familyId;
|
|
|
|
// Fetch children on mount
|
|
useEffect(() => {
|
|
const fetchChildren = async () => {
|
|
if (!familyId) {
|
|
setError('No family found');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('[InsightsDashboard] Loading children for familyId:', familyId);
|
|
const childrenData = await childrenApi.getChildren(familyId);
|
|
console.log('[InsightsDashboard] Loaded children:', childrenData);
|
|
setChildren(childrenData);
|
|
|
|
if (childrenData.length > 0) {
|
|
// Validate selected child or pick first one
|
|
const validChild = childrenData.find(c => c.id === selectedChild);
|
|
if (!validChild) {
|
|
setSelectedChild(childrenData[0].id);
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
console.error('[InsightsDashboard] Failed to load children:', err);
|
|
setError(err.response?.data?.message || t('errors.loadChildren'));
|
|
}
|
|
};
|
|
fetchChildren();
|
|
}, [familyId]);
|
|
|
|
// Fetch activities when child or date range changes
|
|
useEffect(() => {
|
|
if (!selectedChild || children.length === 0) return;
|
|
|
|
// Validate that selectedChild belongs to current user's children
|
|
const childExists = children.some(child => child.id === selectedChild);
|
|
if (!childExists) {
|
|
console.warn('[InsightsDashboard] Selected child not found in user\'s children, resetting');
|
|
setSelectedChild(children[0].id);
|
|
setError('Selected child not found. Showing data for your first child.');
|
|
return;
|
|
}
|
|
|
|
const fetchActivities = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
console.log('[InsightsDashboard] Fetching activities for child:', selectedChild);
|
|
const days = dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 90;
|
|
const endDate = endOfDay(new Date());
|
|
const startDate = startOfDay(subDays(new Date(), days - 1));
|
|
|
|
const activitiesData = await trackingApi.getActivities(
|
|
selectedChild,
|
|
undefined,
|
|
startDate.toISOString(),
|
|
endDate.toISOString()
|
|
);
|
|
console.log('[InsightsDashboard] Fetched activities:', activitiesData.length);
|
|
setActivities(activitiesData);
|
|
} catch (err: any) {
|
|
console.error('[InsightsDashboard] Failed to load activities:', err);
|
|
setError(err.response?.data?.message || t('errors.loadActivities'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchActivities();
|
|
}, [selectedChild, dateRange, children]);
|
|
|
|
// Calculate statistics
|
|
const calculateStats = () => {
|
|
const totalFeedings = activities.filter((a) => a.type === 'feeding').length;
|
|
const totalDiapers = activities.filter((a) => a.type === 'diaper').length;
|
|
|
|
const sleepActivities = activities.filter((a) => a.type === 'sleep');
|
|
const totalSleepMinutes = sleepActivities.reduce((acc, activity) => {
|
|
if (activity.data?.endTime && activity.data?.startTime) {
|
|
const start = parseISO(activity.data.startTime);
|
|
const end = parseISO(activity.data.endTime);
|
|
return acc + differenceInMinutes(end, start);
|
|
}
|
|
return acc;
|
|
}, 0);
|
|
const days = dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 90;
|
|
const avgSleepHours = days > 0 ? (totalSleepMinutes / 60 / days).toFixed(1) : '0.0';
|
|
|
|
const typeCounts: Record<string, number> = {};
|
|
activities.forEach((a) => {
|
|
typeCounts[a.type] = (typeCounts[a.type] || 0) + 1;
|
|
});
|
|
const mostCommonType = Object.entries(typeCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'none';
|
|
|
|
return {
|
|
totalFeedings,
|
|
avgSleepHours,
|
|
totalDiapers,
|
|
mostCommonType,
|
|
};
|
|
};
|
|
|
|
// Prepare chart data
|
|
const prepareDailyData = (): DayData[] => {
|
|
const days = dateRange === '7days' ? 7 : dateRange === '30days' ? 30 : 90;
|
|
const dailyMap = new Map<string, DayData>();
|
|
|
|
for (let i = days - 1; i >= 0; i--) {
|
|
const date = format(subDays(new Date(), i), 'yyyy-MM-dd');
|
|
dailyMap.set(date, {
|
|
date: format(subDays(new Date(), i), 'MMM dd'),
|
|
feedings: 0,
|
|
sleepHours: 0,
|
|
diapers: 0,
|
|
activities: 0,
|
|
});
|
|
}
|
|
|
|
activities.forEach((activity) => {
|
|
const dateKey = format(parseISO(activity.timestamp), 'yyyy-MM-dd');
|
|
const data = dailyMap.get(dateKey);
|
|
if (data) {
|
|
data.activities += 1;
|
|
if (activity.type === 'feeding') data.feedings += 1;
|
|
if (activity.type === 'diaper') data.diapers += 1;
|
|
if (activity.type === 'sleep' && activity.data?.endTime && activity.data?.startTime) {
|
|
const start = parseISO(activity.data.startTime);
|
|
const end = parseISO(activity.data.endTime);
|
|
const hours = differenceInMinutes(end, start) / 60;
|
|
data.sleepHours += hours;
|
|
}
|
|
}
|
|
});
|
|
|
|
return Array.from(dailyMap.values()).map((d) => ({
|
|
...d,
|
|
sleepHours: Number(d.sleepHours.toFixed(1)),
|
|
}));
|
|
};
|
|
|
|
const prepareDiaperData = (): DiaperTypeData[] => {
|
|
const diaperActivities = activities.filter((a) => a.type === 'diaper');
|
|
const typeCount: Record<string, number> = {};
|
|
|
|
diaperActivities.forEach((activity) => {
|
|
const type = activity.data?.type || 'unknown';
|
|
typeCount[type] = (typeCount[type] || 0) + 1;
|
|
});
|
|
|
|
return Object.entries(typeCount).map(([name, value]) => ({
|
|
name: t(`diaperTypes.${name}`),
|
|
value,
|
|
color: COLORS[name as keyof typeof COLORS] || '#CCCCCC',
|
|
}));
|
|
};
|
|
|
|
const prepareActivityTypeData = (): ActivityTypeData[] => {
|
|
const typeCount: Record<string, number> = {};
|
|
|
|
activities.forEach((activity) => {
|
|
typeCount[activity.type] = (typeCount[activity.type] || 0) + 1;
|
|
});
|
|
|
|
return Object.entries(typeCount).map(([name, count]) => ({
|
|
name: t(`activityTypes.${name}`),
|
|
count,
|
|
color: COLORS[name as keyof typeof COLORS] || '#CCCCCC',
|
|
}));
|
|
};
|
|
|
|
const stats = calculateStats();
|
|
const dailyData = prepareDailyData();
|
|
const diaperData = prepareDiaperData();
|
|
const activityTypeData = prepareActivityTypeData();
|
|
const recentActivities = [...activities]
|
|
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
|
.slice(0, 20);
|
|
|
|
const noChildren = children.length === 0;
|
|
const noActivities = activities.length === 0 && !loading;
|
|
|
|
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 }}>
|
|
<Typography variant="h4" sx={{ mb: 1, fontWeight: 600 }}>
|
|
{t('title')}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
|
|
{t('subtitle')}
|
|
</Typography>
|
|
|
|
{/* Time period selector */}
|
|
<Box sx={{ mb: 4, display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
{children.length > 1 && (
|
|
<FormControl sx={{ minWidth: 200 }}>
|
|
<InputLabel>{t('filters.child')}</InputLabel>
|
|
<Select
|
|
value={selectedChild}
|
|
onChange={(e) => setSelectedChild(e.target.value)}
|
|
label={t('filters.child')}
|
|
>
|
|
{children.map((child) => (
|
|
<MenuItem key={child.id} value={child.id}>
|
|
{child.name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
)}
|
|
<ToggleButtonGroup
|
|
value={dateRange}
|
|
exclusive
|
|
onChange={(_, newValue) => newValue && setDateRange(newValue)}
|
|
sx={{
|
|
'& .MuiToggleButton-root': {
|
|
textTransform: 'none',
|
|
fontWeight: 500,
|
|
minWidth: { xs: 80, sm: 120 }
|
|
}
|
|
}}
|
|
>
|
|
<ToggleButton value="7days">{t('filters.dateRange.7days')}</ToggleButton>
|
|
<ToggleButton value="30days">{t('filters.dateRange.30days')}</ToggleButton>
|
|
<ToggleButton value="3months">{t('filters.dateRange.3months')}</ToggleButton>
|
|
</ToggleButtonGroup>
|
|
</Box>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 3 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{noChildren && !loading && (
|
|
<Card>
|
|
<CardContent sx={{ textAlign: 'center', py: 8 }}>
|
|
<ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
|
|
<Typography variant="h6" color="text.secondary" gutterBottom>
|
|
{t('emptyStates.noChildren.title')}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
|
{t('emptyStates.noChildren.message')}
|
|
</Typography>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<Add />}
|
|
onClick={() => router.push('/children')}
|
|
>
|
|
{t('emptyStates.noChildren.action')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{loading && (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 8 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
)}
|
|
|
|
{noActivities && !noChildren && (
|
|
<Alert severity="info" sx={{ mb: 3 }}>
|
|
{t('emptyStates.noActivities')}
|
|
</Alert>
|
|
)}
|
|
|
|
{!loading && !noChildren && !noActivities && (
|
|
<>
|
|
{/* Stats cards */}
|
|
<Grid container spacing={2} sx={{ mb: 4 }} justifyContent="center">
|
|
<Grid item xs={6} sm={3} sx={{ minWidth: 200 }}>
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.3, delay: 0 }}
|
|
>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 3,
|
|
height: '140px',
|
|
minHeight: '140px',
|
|
width: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
textAlign: 'center',
|
|
bgcolor: COLORS.feeding,
|
|
color: 'white',
|
|
}}
|
|
>
|
|
<Box sx={{ fontSize: 48, mb: 1 }} aria-hidden="true">
|
|
<Restaurant sx={{ fontSize: 48 }} />
|
|
</Box>
|
|
<Typography variant="h3" fontWeight={600}>
|
|
{formatNumber(stats.totalFeedings)}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ opacity: 0.9 }}>
|
|
{t('stats.feedings.subtitle')}
|
|
</Typography>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
|
|
<Grid item xs={6} sm={3} sx={{ minWidth: 200 }}>
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.3, delay: 0.1 }}
|
|
>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 3,
|
|
height: '140px',
|
|
minHeight: '140px',
|
|
width: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
textAlign: 'center',
|
|
bgcolor: COLORS.sleep,
|
|
color: 'white',
|
|
}}
|
|
>
|
|
<Box sx={{ fontSize: 48, mb: 1 }} aria-hidden="true">
|
|
<Hotel sx={{ fontSize: 48 }} />
|
|
</Box>
|
|
<Typography variant="h3" fontWeight={600}>
|
|
{formatNumber(stats.avgSleepHours, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}h
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ opacity: 0.9 }}>
|
|
{t('stats.sleep.subtitle')}
|
|
</Typography>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
|
|
<Grid item xs={6} sm={3} sx={{ minWidth: 200 }}>
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.3, delay: 0.2 }}
|
|
>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 3,
|
|
height: '140px',
|
|
minHeight: '140px',
|
|
width: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
textAlign: 'center',
|
|
bgcolor: COLORS.diaper,
|
|
color: 'white',
|
|
}}
|
|
>
|
|
<Box sx={{ fontSize: 48, mb: 1 }} aria-hidden="true">
|
|
<BabyChangingStation sx={{ fontSize: 48 }} />
|
|
</Box>
|
|
<Typography variant="h3" fontWeight={600}>
|
|
{formatNumber(stats.totalDiapers)}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ opacity: 0.9 }}>
|
|
{t('stats.diapers.subtitle')}
|
|
</Typography>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
|
|
<Grid item xs={6} sm={3} sx={{ minWidth: 200 }}>
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.3, delay: 0.3 }}
|
|
>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 3,
|
|
height: '140px',
|
|
minHeight: '140px',
|
|
width: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
textAlign: 'center',
|
|
bgcolor: COLORS.milestone,
|
|
color: 'white',
|
|
}}
|
|
>
|
|
<Box sx={{ fontSize: 48, mb: 1 }} aria-hidden="true">
|
|
<TrendingUp sx={{ fontSize: 48 }} />
|
|
</Box>
|
|
<Typography variant="h3" fontWeight={600} sx={{ textTransform: 'capitalize' }}>
|
|
{t(`activityTypes.${stats.mostCommonType}`)}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ opacity: 0.9 }}>
|
|
{t('stats.topActivity.subtitle')}
|
|
</Typography>
|
|
</Paper>
|
|
</motion.div>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* Charts grid */}
|
|
<Grid container spacing={3} justifyContent="center" sx={{ mb: 3 }}>
|
|
<Grid item xs={12} md={6} sx={{ minWidth: 400 }}>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 3,
|
|
bgcolor: 'background.paper',
|
|
height: 400,
|
|
width: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column'
|
|
}}
|
|
>
|
|
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, fontWeight: 600 }}>
|
|
<Restaurant sx={{ color: COLORS.feeding }} /> {t('charts.feedingFrequency')}
|
|
</Typography>
|
|
<Box sx={{ flexGrow: 1, position: 'relative' }}>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<BarChart data={dailyData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Bar dataKey="feedings" fill={COLORS.feeding} name={t('charts.chartLabels.feedings')} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</Box>
|
|
</Paper>
|
|
</Grid>
|
|
|
|
<Grid item xs={12} md={6} sx={{ minWidth: 400 }}>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 3,
|
|
bgcolor: 'background.paper',
|
|
height: 400,
|
|
width: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column'
|
|
}}
|
|
>
|
|
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, fontWeight: 600 }}>
|
|
<Hotel sx={{ color: COLORS.sleep }} /> {t('charts.sleepDuration')}
|
|
</Typography>
|
|
<Box sx={{ flexGrow: 1, position: 'relative' }}>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<LineChart data={dailyData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="sleepHours"
|
|
stroke={COLORS.sleep}
|
|
strokeWidth={3}
|
|
name={t('charts.chartLabels.sleepHours')}
|
|
dot={{ fill: COLORS.sleep, r: 4 }}
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</Box>
|
|
</Paper>
|
|
</Grid>
|
|
|
|
{diaperData.length > 0 && (
|
|
<Grid item xs={12} md={6} sx={{ minWidth: 400 }}>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 3,
|
|
bgcolor: 'background.paper',
|
|
height: 400,
|
|
width: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column'
|
|
}}
|
|
>
|
|
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, fontWeight: 600 }}>
|
|
<BabyChangingStation sx={{ color: COLORS.diaper }} /> {t('charts.diaperChangesByType')}
|
|
</Typography>
|
|
<Box sx={{ flexGrow: 1, position: 'relative' }}>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<PieChart>
|
|
<Pie
|
|
data={diaperData}
|
|
cx="50%"
|
|
cy="50%"
|
|
labelLine={false}
|
|
label={({ name, percent }: any) => `${name} ${(percent * 100).toFixed(0)}%`}
|
|
outerRadius={80}
|
|
fill="#8884d8"
|
|
dataKey="value"
|
|
>
|
|
{diaperData.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</Box>
|
|
</Paper>
|
|
</Grid>
|
|
)}
|
|
|
|
<Grid item xs={12} md={diaperData.length > 0 ? 6 : 12} sx={{ minWidth: 400 }}>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
p: 3,
|
|
bgcolor: 'background.paper',
|
|
height: 400,
|
|
width: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column'
|
|
}}
|
|
>
|
|
<Typography variant="h6" sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 1, fontWeight: 600 }}>
|
|
<Assessment sx={{ color: 'primary.main' }} /> {t('charts.activityTimeline')}
|
|
</Typography>
|
|
<Box sx={{ flexGrow: 1, position: 'relative' }}>
|
|
<ResponsiveContainer width="100%" height={250}>
|
|
<BarChart data={dailyData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Legend />
|
|
<Bar dataKey="feedings" fill={COLORS.feeding} name={t('charts.chartLabels.feedings')} />
|
|
<Bar dataKey="diapers" fill={COLORS.diaper} name={t('charts.chartLabels.diapers')} />
|
|
<Bar dataKey="sleepHours" fill={COLORS.sleep} name={t('charts.chartLabels.sleepHours')} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</Box>
|
|
</Paper>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{activityTypeData.length > 0 && (
|
|
<Card sx={{ mb: 3 }}>
|
|
<CardContent>
|
|
<Typography variant="h6" fontWeight="600" gutterBottom>
|
|
{t('charts.activityDistribution')}
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', mt: 2 }}>
|
|
{activityTypeData.map((activity) => (
|
|
<Chip
|
|
key={activity.name}
|
|
icon={getActivityIcon(activity.name.toLowerCase() as ActivityType)}
|
|
label={`${activity.name}: ${activity.count}`}
|
|
sx={{
|
|
bgcolor: activity.color,
|
|
color: 'white',
|
|
fontSize: '0.9rem',
|
|
fontWeight: 600,
|
|
px: 1,
|
|
py: 2,
|
|
}}
|
|
/>
|
|
))}
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Recent Activities */}
|
|
<Card>
|
|
<CardContent>
|
|
<Typography variant="h6" fontWeight="600" gutterBottom>
|
|
{t('recentActivities.title')}
|
|
</Typography>
|
|
<Divider sx={{ my: 2 }} />
|
|
<List sx={{ maxHeight: 400, overflow: 'auto' }}>
|
|
{recentActivities.map((activity, index) => (
|
|
<motion.div
|
|
key={activity.id}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.3, delay: index * 0.02 }}
|
|
>
|
|
<ListItem
|
|
sx={{
|
|
borderBottom: index < recentActivities.length - 1 ? '1px solid' : 'none',
|
|
borderColor: 'divider',
|
|
py: 1.5,
|
|
}}
|
|
>
|
|
<ListItemAvatar>
|
|
<Avatar sx={{ bgcolor: getActivityColor(activity.type) }}>
|
|
{getActivityIcon(activity.type)}
|
|
</Avatar>
|
|
</ListItemAvatar>
|
|
<ListItemText
|
|
primary={
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<Typography variant="body1" fontWeight="600" sx={{ textTransform: 'capitalize' }}>
|
|
{t(`activityTypes.${activity.type}`)}
|
|
</Typography>
|
|
<Chip
|
|
label={formatDistanceToNow(parseISO(activity.timestamp), { addSuffix: true })}
|
|
size="small"
|
|
sx={{ height: 20, fontSize: '0.7rem' }}
|
|
/>
|
|
</Box>
|
|
}
|
|
secondary={
|
|
<Typography variant="body2" color="text.secondary">
|
|
{activity.notes || format(parseISO(activity.timestamp), 'MMM dd, yyyy HH:mm')}
|
|
</Typography>
|
|
}
|
|
/>
|
|
</ListItem>
|
|
</motion.div>
|
|
))}
|
|
</List>
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
)}
|
|
</Box>
|
|
</motion.div>
|
|
);
|
|
};
|