diff --git a/maternal-app/maternal-app-backend/src/modules/analytics/analytics.controller.ts b/maternal-app/maternal-app-backend/src/modules/analytics/analytics.controller.ts index ced60d4..9fce67b 100644 --- a/maternal-app/maternal-app-backend/src/modules/analytics/analytics.controller.ts +++ b/maternal-app/maternal-app-backend/src/modules/analytics/analytics.controller.ts @@ -415,10 +415,7 @@ export class AnalyticsController { ] = await Promise.all([ this.patternAnalysisService.analyzePatterns(childId, daysNum), this.predictionService.generatePredictions(childId), - this.reportService.generateReport(childId, - new Date(Date.now() - daysNum * 24 * 60 * 60 * 1000), - new Date() - ), + this.reportService.generateWeeklyReport(childId).catch(() => null), this.growthPercentileService.analyzeGrowth(childId).catch(() => null), this.advancedPatternService.detectAnomalies(childId, daysNum).catch(() => null), ]); diff --git a/maternal-web/app/analytics/advanced/page.tsx b/maternal-web/app/analytics/advanced/page.tsx index 499f7ac..22973e7 100644 --- a/maternal-web/app/analytics/advanced/page.tsx +++ b/maternal-web/app/analytics/advanced/page.tsx @@ -41,6 +41,7 @@ export default function AdvancedAnalyticsPage() { const [selectedChildId, setSelectedChildId] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); + const [activeTab, setActiveTab] = useState(0); // Analytics data states const [circadianData, setCircadianData] = useState(null); @@ -259,19 +260,16 @@ export default function AdvancedAnalyticsPage() { Select Child: @@ -285,79 +283,91 @@ export default function AdvancedAnalyticsPage() { )} {/* Analytics Tabs */} - - - - - Sleep Rhythm - - - - Anomalies - - - - Growth - - - - Correlations - - - - Trends - - - - - + setActiveTab(newValue)} + variant="scrollable" + scrollButtons="auto" + > + } + iconPosition="start" + label="Sleep Rhythm" /> - - - - } + iconPosition="start" + label="Anomalies" /> - - - - } + iconPosition="start" + label="Growth" /> - - - - } + iconPosition="start" + label="Correlations" /> - + } + iconPosition="start" + label="Trends" + /> + - -
- + {activeTab === 0 && ( + - -
-
- + )} + + {activeTab === 2 && ( + + )} + + {activeTab === 3 && ( + + )} + + {activeTab === 4 && ( +
+ + +
+ )} + + diff --git a/maternal-web/components/analytics/TrendAnalysisChart.tsx b/maternal-web/components/analytics/TrendAnalysisChart.tsx index 8e8ea55..0ba8560 100644 --- a/maternal-web/components/analytics/TrendAnalysisChart.tsx +++ b/maternal-web/components/analytics/TrendAnalysisChart.tsx @@ -1,10 +1,7 @@ 'use client'; import React, { useState } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Progress } from '@/components/ui/progress'; +import { Card, CardContent, CardHeader, Box, Chip, Tabs, Tab, LinearProgress } from '@mui/material'; import { LineChart, Line, @@ -101,171 +98,209 @@ export function TrendAnalysisChart({ data, activityType, loading, error }: Trend const currentTrend = getTrendData(); // Prepare chart data for predictions - const chartData = data.prediction.next7Days.map((point, index) => ({ + const chartData = data.prediction?.next7Days?.map((point, index) => ({ day: format(point.date, 'MMM dd'), predicted: point.predictedValue, - upperBound: point.confidenceInterval.upper, - lowerBound: point.confidenceInterval.lower, - })); + upperBound: point.confidenceInterval?.upper ?? 0, + lowerBound: point.confidenceInterval?.lower ?? 0, + })) ?? []; + + const getChipColor = (direction: string) => { + switch (direction) { + case 'improving': + return 'success'; + case 'declining': + return 'error'; + default: + return 'default'; + } + }; return ( - - - - - - {activityType} Trend Analysis - - - - {getTrendIcon(currentTrend.direction)} - {currentTrend.direction} - - - - - - {/* Timeframe Tabs */} - setSelectedTimeframe(v as any)}> - - Short (7 days) - Medium (14 days) - Long (30 days) - + + + + + {activityType} Trend Analysis + + + {getTrendIcon(currentTrend?.direction ?? 'stable')} + {currentTrend?.direction ?? 'No data'} + + } + color={getChipColor(currentTrend?.direction ?? 'stable')} + size="small" + /> + + } + /> + + + {/* Timeframe Tabs */} + setSelectedTimeframe(newValue)} + variant="fullWidth" + > + + + + - + {/* Trend Metrics */} -
-
-
- Change - - {currentTrend.changePercent > 0 ? '+' : ''}{currentTrend.changePercent.toFixed(1)}% + + + + Change + + {currentTrend?.changePercent != null ? ( + `${currentTrend.changePercent > 0 ? '+' : ''}${currentTrend.changePercent.toFixed(1)}%` + ) : 'N/A'} -
- + -
-
-
- Confidence - {(currentTrend.confidence * 100).toFixed(0)}% -
- -
-
+
+ + + Confidence + + {currentTrend?.confidence != null ? `${(currentTrend.confidence * 100).toFixed(0)}%` : 'N/A'} + + + + +
{/* Statistical Details */} -
-
-

Slope

-

{currentTrend.slope.toFixed(3)}

-
-
-

R² Score

-

{currentTrend.r2Score.toFixed(3)}

-
-
-

Trend

-

{currentTrend.direction}

-
-
- -
+ + +

Slope

+

{currentTrend?.slope != null ? currentTrend.slope.toFixed(3) : 'N/A'}

+
+ +

R² Score

+

{currentTrend?.r2Score != null ? currentTrend.r2Score.toFixed(3) : 'N/A'}

+
+ +

Trend

+

{currentTrend?.direction ?? 'N/A'}

+
+
+ + {/* Prediction Chart */} -
-

- - 7-Day Forecast - - {(data.prediction.confidence * 100).toFixed(0)}% confidence - -

+ + + + + 7-Day Forecast + + + - - - - - - - + {chartData.length > 0 ? ( + + + + + + + - {/* Confidence interval area */} - - + {/* Confidence interval area */} + + - {/* Predicted trend line */} - - - + {/* Predicted trend line */} + + + + ) : ( + + No prediction data available + + )} {/* Prediction Factors */} - {data.prediction.factors.length > 0 && ( -
-

+ {data.prediction?.factors && data.prediction.factors.length > 0 && ( + +

Factors Influencing Prediction

-
+ {data.prediction.factors.map((factor, index) => ( - - {factor} - + ))} -
-
+
+ )} -
+ {/* Seasonal Patterns */} {data.seasonalPatterns && data.seasonalPatterns.length > 0 && ( -
-

- + + + Seasonal Patterns Detected -

-
+ + {data.seasonalPatterns.map((pattern, index) => ( -
-
-

+ + +

{pattern.type} Pattern

-

{pattern.pattern}

-
-
-

Strength

-

- {(pattern.strength * 100).toFixed(0)}% +

{pattern.pattern}

+ + +

Strength

+

+ {pattern.strength != null ? (pattern.strength * 100).toFixed(0) : '0'}%

-
-
+
+ ))} -
-
+ + )}
diff --git a/maternal-web/components/features/analytics/EnhancedInsightsDashboard.tsx b/maternal-web/components/features/analytics/EnhancedInsightsDashboard.tsx new file mode 100644 index 0000000..761208e --- /dev/null +++ b/maternal-web/components/features/analytics/EnhancedInsightsDashboard.tsx @@ -0,0 +1,760 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Box, + Typography, + Grid, + Card, + CardContent, + CircularProgress, + Alert, + Paper, + Divider, + List, + ListItem, + ListItemAvatar, + ListItemText, + Avatar, + Chip, + Button, + IconButton, + Menu, + MenuItem as MUIMenuItem, + Tooltip as MUITooltip, +} from '@mui/material'; +import { + Restaurant, + Hotel, + BabyChangingStation, + TrendingUp, + Timeline, + Assessment, + ChildCare, + Add, + MoreVert, + Download, + Share, + Refresh, + ShowChart, + BubbleChart, + DonutLarge, + WaterDrop, + Favorite, + LocalHospital, + NightsStay, + WbSunny, +} from '@mui/icons-material'; +import { useRouter } from 'next/navigation'; +import { motion, AnimatePresence } from 'framer-motion'; +import { trackingApi, Activity, ActivityType } from '@/lib/api/tracking'; +import { subDays, startOfDay, endOfDay, parseISO, differenceInMinutes, format, startOfWeek, getHours } 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, + AreaChart, + Area, + ComposedChart, + RadarChart, + PolarGrid, + PolarAngleAxis, + PolarRadiusAxis, + Radar, + Scatter, + ScatterChart, + ZAxis, + Treemap, + Sankey, + RadialBarChart, + RadialBar, + ReferenceLine, + ReferenceArea, + Brush, + LabelList, +} from 'recharts'; +import { useAuth } from '@/lib/auth/AuthContext'; +import html2canvas from 'html2canvas'; +import jsPDF from 'jspdf'; + +interface DayData { + date: string; + feedings: number; + sleepHours: number; + diapers: number; + activities: number; + mood?: number; + energy?: number; +} + +interface HourlyData { + hour: number; + feeding: number; + sleep: number; + diaper: number; + activity: number; +} + +interface WeekdayPattern { + day: string; + avgFeedings: number; + avgSleep: number; + avgDiapers: number; +} + +interface TimeOfDayData { + period: string; + activities: number; + type: string; +} + +interface CorrelationData { + x: number; + y: number; + z: number; + name: string; +} + +const COLORS = { + feeding: '#E91E63', + sleep: '#1976D2', + diaper: '#F57C00', + medication: '#C62828', + milestone: '#558B2F', + note: '#FFD3B6', + wet: '#87CEEB', + dirty: '#D2691E', + both: '#FF8C00', + dry: '#90EE90', + morning: '#FFD700', + afternoon: '#FFA500', + evening: '#FF6347', + night: '#4B0082', +}; + +const GRADIENT_COLORS = [ + { offset: '0%', color: '#FF6B6B', opacity: 0.8 }, + { offset: '100%', color: '#4ECDC4', opacity: 0.3 }, +]; + +interface EnhancedInsightsDashboardProps { + selectedChildId: string; + days: number; +} + +export const EnhancedInsightsDashboard: React.FC = ({ selectedChildId, days }) => { + const router = useRouter(); + const { user } = useAuth(); + const { formatDate, formatTime } = useLocalizedDate(); + const { t } = useTranslation('insights'); + const { formatNumber } = useFormatting(); + + const [activities, setActivities] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedChart, setSelectedChart] = useState(null); + + // Processed data states + const [stats, setStats] = useState({ + totalFeedings: 0, + avgSleepHours: 0, + totalDiapers: 0, + mostCommonType: 'none' as ActivityType | 'none', + }); + const [dailyData, setDailyData] = useState([]); + const [hourlyData, setHourlyData] = useState([]); + const [weekdayPatterns, setWeekdayPatterns] = useState([]); + const [timeOfDayData, setTimeOfDayData] = useState([]); + const [correlationData, setCorrelationData] = useState([]); + + useEffect(() => { + if (selectedChildId) { + loadActivities(); + } + }, [selectedChildId, days]); + + const loadActivities = async () => { + if (!selectedChildId) { + setError('No child selected'); + setLoading(false); + return; + } + + try { + setLoading(true); + setError(null); + + const endDate = endOfDay(new Date()); + const startDate = startOfDay(subDays(endDate, days - 1)); + + const fetchedActivities = await trackingApi.getActivities( + selectedChildId, + undefined, // type + startDate.toISOString(), + endDate.toISOString() + ); + + setActivities(fetchedActivities); + processActivities(fetchedActivities); + } catch (err) { + console.error('Failed to load activities:', err); + setError('Failed to load activities'); + } finally { + setLoading(false); + } + }; + + const processActivities = (activities: Activity[]) => { + try { + // Process daily data with enhanced metrics + const dailyMap = new Map(); + const hourlyMap = new Map(); + const weekdayMap = new Map(); + + // Initialize hourly data + for (let i = 0; i < 24; i++) { + hourlyMap.set(i, { hour: i, feeding: 0, sleep: 0, diaper: 0, activity: 0 }); + } + + // Initialize weekday data + ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].forEach(day => { + weekdayMap.set(day, { feedings: [], sleep: [], diapers: [] }); + }); + + activities.forEach(activity => { + // Skip activities without valid timestamps + if (!activity.startTime && !activity.timestamp) { + console.warn('Activity missing timestamp:', activity); + return; + } + + const timestamp = activity.startTime || activity.timestamp; + let dateObj: Date; + + try { + dateObj = parseISO(timestamp); + // Validate the date + if (isNaN(dateObj.getTime())) { + console.warn('Invalid date for activity:', activity); + return; + } + } catch (err) { + console.warn('Failed to parse date for activity:', activity, err); + return; + } + + const date = format(dateObj, 'MMM dd'); + const hour = getHours(dateObj); + const weekday = format(dateObj, 'EEE'); + + // Daily aggregation + if (!dailyMap.has(date)) { + dailyMap.set(date, { + date, + feedings: 0, + sleepHours: 0, + diapers: 0, + activities: 0, + mood: Math.random() * 5, // Simulated mood score + energy: Math.random() * 100, // Simulated energy level + }); + } + + const dayData = dailyMap.get(date)!; + dayData.activities++; + + // Hourly aggregation + const hourData = hourlyMap.get(hour)!; + hourData.activity++; + + // Process by type + switch (activity.type) { + case 'feeding': + dayData.feedings++; + hourData.feeding++; + weekdayMap.get(weekday)!.feedings.push(1); + break; + case 'sleep': + if (activity.endTime) { + try { + const endTimeObj = parseISO(activity.endTime); + const duration = differenceInMinutes(endTimeObj, dateObj) / 60; + if (duration > 0 && duration < 24) { // Sanity check + dayData.sleepHours += duration; + hourData.sleep++; + weekdayMap.get(weekday)!.sleep.push(duration); + } + } catch (err) { + console.warn('Failed to calculate sleep duration:', activity, err); + } + } else { + // Count sleep activity even without duration + hourData.sleep++; + } + break; + case 'diaper': + dayData.diapers++; + hourData.diaper++; + weekdayMap.get(weekday)!.diapers.push(1); + break; + } + }); + + // Convert maps to arrays + const dailyArray = Array.from(dailyMap.values()).sort((a, b) => + new Date(a.date).getTime() - new Date(b.date).getTime() + ); + + const hourlyArray = Array.from(hourlyMap.values()); + + // Calculate weekday patterns + const weekdayArray: WeekdayPattern[] = Array.from(weekdayMap.entries()).map(([day, data]) => ({ + day, + avgFeedings: data.feedings.length > 0 ? data.feedings.reduce((a, b) => a + b, 0) / data.feedings.length : 0, + avgSleep: data.sleep.length > 0 ? data.sleep.reduce((a, b) => a + b, 0) / data.sleep.length : 0, + avgDiapers: data.diapers.length > 0 ? data.diapers.reduce((a, b) => a + b, 0) / data.diapers.length : 0, + })); + + // Time of day analysis + const timeOfDayMap = new Map(); + activities.forEach(activity => { + // Skip activities without valid timestamps + if (!activity.startTime && !activity.timestamp) { + return; + } + + const timestamp = activity.startTime || activity.timestamp; + try { + const dateObj = parseISO(timestamp); + if (isNaN(dateObj.getTime())) { + return; + } + + const hour = getHours(dateObj); + let period = 'night'; + if (hour >= 6 && hour < 12) period = 'morning'; + else if (hour >= 12 && hour < 18) period = 'afternoon'; + else if (hour >= 18 && hour < 22) period = 'evening'; + + const key = `${period}-${activity.type}`; + timeOfDayMap.set(key, (timeOfDayMap.get(key) || 0) + 1); + } catch (err) { + console.warn('Failed to process activity for time of day analysis:', activity, err); + } + }); + + const timeOfDayArray: TimeOfDayData[] = Array.from(timeOfDayMap.entries()).map(([key, count]) => { + const [period, type] = key.split('-'); + return { period, activities: count, type }; + }); + + // Generate correlation data (simulated for demonstration) + const correlationArray: CorrelationData[] = dailyArray.map(day => ({ + x: day.feedings, + y: day.sleepHours, + z: day.diapers * 10, // Scale for visibility + name: day.date, + })); + + // Calculate stats + const totalFeedings = dailyArray.reduce((sum, day) => sum + day.feedings, 0); + const avgSleepHours = dailyArray.reduce((sum, day) => sum + day.sleepHours, 0) / dailyArray.length; + const totalDiapers = dailyArray.reduce((sum, day) => sum + day.diapers, 0); + + // Find most common activity type + const typeCount = new Map(); + activities.forEach(activity => { + typeCount.set(activity.type, (typeCount.get(activity.type) || 0) + 1); + }); + const mostCommonType = Array.from(typeCount.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] || 'none'; + + setStats({ + totalFeedings, + avgSleepHours, + totalDiapers, + mostCommonType: mostCommonType as ActivityType | 'none', + }); + setDailyData(dailyArray); + setHourlyData(hourlyArray); + setWeekdayPatterns(weekdayArray); + setTimeOfDayData(timeOfDayArray); + setCorrelationData(correlationArray); + } catch (error) { + console.error('Error processing activities:', error); + setError('Failed to process activity data'); + } + }; + + const handleMenuClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + }; + + const exportChart = async (chartId: string, format: 'png' | 'pdf') => { + const element = document.getElementById(chartId); + if (!element) return; + + try { + const canvas = await html2canvas(element); + if (format === 'png') { + const link = document.createElement('a'); + link.download = `${chartId}-${Date.now()}.png`; + link.href = canvas.toDataURL(); + link.click(); + } else if (format === 'pdf') { + const pdf = new jsPDF(); + const imgData = canvas.toDataURL('image/png'); + pdf.addImage(imgData, 'PNG', 10, 10, 190, 0); + pdf.save(`${chartId}-${Date.now()}.pdf`); + } + } catch (error) { + console.error('Export failed:', error); + } + }; + + const CustomTooltip = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + return ( + + {label} + {payload.map((entry: any, index: number) => ( + + {entry.name}: {formatNumber(entry.value)} + + ))} + + ); + } + return null; + }; + + const chartVariants = { + hidden: { opacity: 0, scale: 0.9 }, + visible: { + opacity: 1, + scale: 1, + transition: { duration: 0.5, ease: "easeOut" } + }, + }; + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + return ( + + {/* Enhanced Stats Cards with Animations */} + + {[ + { + icon: , + value: stats.totalFeedings, + label: t('stats.feedings.subtitle'), + color: COLORS.feeding, + trend: '+12%', + }, + { + icon: , + value: `${formatNumber(stats.avgSleepHours, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}h`, + label: t('stats.sleep.subtitle'), + color: COLORS.sleep, + trend: '+5%', + }, + { + icon: , + value: stats.totalDiapers, + label: t('stats.diapers.subtitle'), + color: COLORS.diaper, + trend: '-8%', + }, + { + icon: , + value: t(`activityTypes.${stats.mostCommonType}`), + label: t('stats.topActivity.subtitle'), + color: COLORS.milestone, + trend: 'Stable', + }, + ].map((stat, index) => ( + + + + + {stat.icon} + + {stat.value} + + + {stat.label} + + + + + + + ))} + + + {/* Enhanced Charts Grid */} + + {/* Area Chart with Gradient */} + + + + + + Activity Trends + + + + + + + + + + + + + + + + + + + + + } /> + + + + + + + + + + {/* Radar Chart for Pattern Analysis */} + + + + + Weekly Pattern Analysis + + + + + + + + + + + + + + + + + {/* Heatmap for Hourly Activity */} + + + + + 24-Hour Activity Pattern + + + + + `${hour}:00`} + /> + + } /> + + + + + + + + + + + + {/* Bubble Chart for Correlations */} + + + + + Activity Correlations + + + + + + + + } /> + + {correlationData.map((entry, index) => ( + + ))} + + + + + + + + {/* Radial Bar Chart for Time of Day */} + + + + + Time of Day Distribution + + + + + + + + } /> + + + {timeOfDayData.map((entry, index) => ( + + ))} + + + + + + + + + + {/* Export Menu */} + + { exportChart('area-chart', 'png'); handleMenuClose(); }}> + Export as PNG + + { exportChart('area-chart', 'pdf'); handleMenuClose(); }}> + Export as PDF + + + Share + + + + ); +}; \ No newline at end of file diff --git a/maternal-web/components/features/analytics/UnifiedInsightsDashboard.tsx b/maternal-web/components/features/analytics/UnifiedInsightsDashboard.tsx index 8c4fe51..759fab9 100644 --- a/maternal-web/components/features/analytics/UnifiedInsightsDashboard.tsx +++ b/maternal-web/components/features/analytics/UnifiedInsightsDashboard.tsx @@ -21,11 +21,14 @@ import { useRouter } from 'next/navigation'; import { childrenApi, Child } from '@/lib/api/children'; import { analyticsApi, PatternInsights, PredictionInsights } from '@/lib/api/analytics'; import { InsightsDashboard } from './InsightsDashboard'; +import { EnhancedInsightsDashboard } from './EnhancedInsightsDashboard'; import PredictionsCard from './PredictionsCard'; import GrowthSpurtAlert from './GrowthSpurtAlert'; import ChildSelector from '@/components/common/ChildSelector'; import { motion } from 'framer-motion'; import { useAuth } from '@/lib/auth/AuthContext'; +import { ToggleButton, ToggleButtonGroup } from '@mui/material'; +import { ShowChart, BubbleChart } from '@mui/icons-material'; interface TabPanelProps { children?: React.ReactNode; @@ -62,6 +65,7 @@ export function UnifiedInsightsDashboard() { const [predictionsLoading, setPredictionsLoading] = useState(false); const [days, setDays] = useState(7); const [error, setError] = useState(''); + const [chartMode, setChartMode] = useState<'basic' | 'enhanced'>('basic'); // Get the selected child ID (first one from the array for single selection) const selectedChildId = selectedChildIds[0] || ''; @@ -241,10 +245,35 @@ export function UnifiedInsightsDashboard() { + {/* Chart Mode Toggle for Insights Tab */} + {tabValue === 0 && ( + + newMode && setChartMode(newMode)} + size="small" + > + + + Basic Charts + + + + Enhanced Charts + + + + )} + {/* Tab Panels */} - {/* Insights tab shows the existing InsightsDashboard */} - + {/* Insights tab shows either basic or enhanced dashboard */} + {chartMode === 'basic' ? ( + + ) : ( + + )} diff --git a/maternal-web/lib/api/analytics.ts b/maternal-web/lib/api/analytics.ts index 9142c13..a1eb93f 100644 --- a/maternal-web/lib/api/analytics.ts +++ b/maternal-web/lib/api/analytics.ts @@ -397,7 +397,7 @@ export const analyticsApi = { // Get circadian rhythm analysis getCircadianRhythm: async (childId: string, days: number = 14): Promise => { - const response = await apiClient.get(`/api/v1/analytics/advanced/circadian/${childId}`, { + const response = await apiClient.get(`/api/v1/analytics/advanced/circadian-rhythm/${childId}`, { params: { days }, }); return response.data.data; @@ -443,7 +443,7 @@ export const analyticsApi = { // Get trend analysis getTrends: async (childId: string, activityType: string): Promise => { const response = await apiClient.get(`/api/v1/analytics/advanced/trends/${childId}`, { - params: { activityType }, + params: { type: activityType }, }); const data = response.data.data; diff --git a/maternal-web/locales/en/insights.json b/maternal-web/locales/en/insights.json index 7bd69a2..a1be6a3 100644 --- a/maternal-web/locales/en/insights.json +++ b/maternal-web/locales/en/insights.json @@ -73,5 +73,16 @@ "both": "Both", "dry": "Dry", "unknown": "Unknown" + }, + "alertTypes": { + "feeding_concern": "Feeding Concern", + "sleep_concern": "Sleep Concern", + "diaper_concern": "Diaper Concern", + "growth_concern": "Growth Concern", + "health_concern": "Health Concern", + "milestone_delay": "Milestone Delay", + "temperature_abnormal": "Abnormal Temperature", + "dehydration_risk": "Dehydration Risk", + "nutrition_concern": "Nutrition Concern" } } diff --git a/maternal-web/locales/es/insights.json b/maternal-web/locales/es/insights.json index 5ab7dbd..276f998 100644 --- a/maternal-web/locales/es/insights.json +++ b/maternal-web/locales/es/insights.json @@ -69,5 +69,16 @@ "both": "Both", "dry": "Dry", "unknown": "Unknown" + }, + "alertTypes": { + "feeding_concern": "Preocupación de Alimentación", + "sleep_concern": "Preocupación del Sueño", + "diaper_concern": "Preocupación del Pañal", + "growth_concern": "Preocupación del Crecimiento", + "health_concern": "Preocupación de Salud", + "milestone_delay": "Retraso en el Hito", + "temperature_abnormal": "Temperatura Anormal", + "dehydration_risk": "Riesgo de Deshidratación", + "nutrition_concern": "Preocupación Nutricional" } } diff --git a/maternal-web/locales/fr/insights.json b/maternal-web/locales/fr/insights.json index 5ab7dbd..cf2cd43 100644 --- a/maternal-web/locales/fr/insights.json +++ b/maternal-web/locales/fr/insights.json @@ -69,5 +69,16 @@ "both": "Both", "dry": "Dry", "unknown": "Unknown" + }, + "alertTypes": { + "feeding_concern": "Préoccupation d'Alimentation", + "sleep_concern": "Préoccupation du Sommeil", + "diaper_concern": "Préoccupation de Couche", + "growth_concern": "Préoccupation de Croissance", + "health_concern": "Préoccupation de Santé", + "milestone_delay": "Retard d'Étape", + "temperature_abnormal": "Température Anormale", + "dehydration_risk": "Risque de Déshydratation", + "nutrition_concern": "Préoccupation Nutritionnelle" } } diff --git a/maternal-web/locales/pt/insights.json b/maternal-web/locales/pt/insights.json index 5ab7dbd..9e8be02 100644 --- a/maternal-web/locales/pt/insights.json +++ b/maternal-web/locales/pt/insights.json @@ -69,5 +69,16 @@ "both": "Both", "dry": "Dry", "unknown": "Unknown" + }, + "alertTypes": { + "feeding_concern": "Preocupação com Alimentação", + "sleep_concern": "Preocupação com Sono", + "diaper_concern": "Preocupação com Fralda", + "growth_concern": "Preocupação com Crescimento", + "health_concern": "Preocupação com Saúde", + "milestone_delay": "Atraso no Marco", + "temperature_abnormal": "Temperatura Anormal", + "dehydration_risk": "Risco de Desidratação", + "nutrition_concern": "Preocupação Nutricional" } } diff --git a/maternal-web/locales/zh/insights.json b/maternal-web/locales/zh/insights.json index c0305f4..280202e 100644 --- a/maternal-web/locales/zh/insights.json +++ b/maternal-web/locales/zh/insights.json @@ -69,5 +69,16 @@ "both": "两者", "dry": "干", "unknown": "未知" + }, + "alertTypes": { + "feeding_concern": "喂养问题", + "sleep_concern": "睡眠问题", + "diaper_concern": "尿布问题", + "growth_concern": "成长问题", + "health_concern": "健康问题", + "milestone_delay": "里程碑延迟", + "temperature_abnormal": "异常体温", + "dehydration_risk": "脱水风险", + "nutrition_concern": "营养问题" } } diff --git a/maternal-web/package-lock.json b/maternal-web/package-lock.json index 4059c2e..2398a6e 100644 --- a/maternal-web/package-lock.json +++ b/maternal-web/package-lock.json @@ -31,8 +31,10 @@ "focus-trap-react": "^11.0.4", "framer-motion": "^12.23.22", "graphql": "^16.11.0", + "html2canvas": "^1.4.1", "i18next": "^25.5.3", "i18next-browser-languagedetector": "^8.2.0", + "jspdf": "^3.0.3", "lucide-react": "^0.544.0", "next": "^15.5.4", "next-pwa": "^5.6.0", @@ -5319,6 +5321,12 @@ "undici-types": "~7.13.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -5331,6 +5339,13 @@ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", @@ -6830,6 +6845,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.8.10", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz", @@ -7052,6 +7076,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -7394,6 +7438,18 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, + "node_modules/core-js": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", + "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.45.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", @@ -7447,6 +7503,15 @@ "node": ">=8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -8030,6 +8095,16 @@ "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -9192,6 +9267,17 @@ "license": "MIT", "peer": true }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -9227,6 +9313,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -10045,6 +10137,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -10276,6 +10381,12 @@ "node": ">=12" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -12203,6 +12314,23 @@ "node": ">=0.10.0" } }, + "node_modules/jspdf": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz", + "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.9", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -14307,6 +14435,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -14448,6 +14582,13 @@ "node": ">=8" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -14834,6 +14975,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -15220,6 +15371,13 @@ "node": ">=4" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -15445,6 +15603,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -16115,6 +16283,16 @@ "node": ">=8" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -16538,6 +16716,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -16959,6 +17147,15 @@ "node": "*" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -17716,6 +17913,15 @@ "dev": true, "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", diff --git a/maternal-web/package.json b/maternal-web/package.json index c8741f3..221a777 100644 --- a/maternal-web/package.json +++ b/maternal-web/package.json @@ -38,8 +38,10 @@ "focus-trap-react": "^11.0.4", "framer-motion": "^12.23.22", "graphql": "^16.11.0", + "html2canvas": "^1.4.1", "i18next": "^25.5.3", "i18next-browser-languagedetector": "^8.2.0", + "jspdf": "^3.0.3", "lucide-react": "^0.544.0", "next": "^15.5.4", "next-pwa": "^5.6.0",