Files
maternal-app/maternal-web/components/analytics/FeedingFrequencyGraph.tsx
Andrei b56f9546c2 feat: Complete high-priority i18n localization with date/time support
This commit implements comprehensive localization for high-priority components:

## Tracking Pages (4 files)
- Localized feeding, sleep, diaper, and medicine tracking pages
- Replaced hardcoded strings with translation keys from tracking namespace
- Added useTranslation hook integration
- All form labels, buttons, and messages now support multiple languages

## Child Dialog Components (2 files)
- Localized ChildDialog (add/edit child form)
- Localized DeleteConfirmDialog
- Added new translation keys to children.json for dialog content
- Includes validation messages and action buttons

## Date/Time Localization (14 files + new hook)
- Created useLocalizedDate hook wrapping date-fns with locale support
- Supports 5 languages: English, Spanish, French, Portuguese, Chinese
- Updated all date formatting across:
  * Tracking pages (feeding, sleep, diaper, medicine)
  * Activity pages (activities, history, track activity)
  * Settings components (sessions, biometric, device trust)
  * Analytics components (insights, growth, sleep chart, feeding graph)
- Date displays automatically adapt to user's language (e.g., "2 hours ago" → "hace 2 horas")

## Translation Updates
- Enhanced children.json with dialog section containing:
  * Form field labels (name, birthDate, gender, photoUrl)
  * Action buttons (add, update, delete, cancel, saving, deleting)
  * Delete confirmation messages
  * Validation error messages

Files changed: 17 files (+164, -113)
Languages supported: en, es, fr, pt-BR, zh-CN

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 11:49:48 +00:00

278 lines
8.0 KiB
TypeScript

'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 { subDays } from 'date-fns';
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
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;
[key: string]: string | number;
}
const COLORS = {
breastfeeding: '#FFB6C1',
bottle: '#FFA5B0',
solids: '#FF94A5',
};
export default function FeedingFrequencyGraph() {
const { format } = useLocalizedDate();
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 }: any) => `${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>
);
}