Files
maternal-app/maternal-web/app/page.tsx
Andrei b94f298d2b fix: Replace GraphQL with REST API for home page dashboard
GraphQL endpoint was returning 400 errors due to authentication issues
with the GqlAuthGuard. Replaced GraphQL query with REST API calls to
match the working insights page pattern.

Changes:
- Removed useQuery(GET_DASHBOARD) GraphQL call
- Added REST API calls: childrenApi.getChildren(), trackingApi.getActivities()
- Calculate today's summary from activities client-side
- Load children and dashboard data separately
- Removed all GraphQL debug logging

Now home page uses same REST pattern as insights page:
1. Load children via childrenApi
2. Load activities via trackingApi
3. Calculate summary from filtered activities

This eliminates the GraphQL 400 errors and makes Today's Summary
display correctly with feeding count, sleep duration, diaper count.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 07:11:29 +00:00

366 lines
14 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import { Box, Typography, Button, Paper, CircularProgress, Grid } from '@mui/material';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { EmailVerificationBanner } from '@/components/common/EmailVerificationBanner';
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
import { DataErrorFallback } from '@/components/common/ErrorFallbacks';
import { NetworkStatusIndicator } from '@/components/common/NetworkStatusIndicator';
import { StatGridSkeleton } from '@/components/common/LoadingSkeletons';
import DynamicChildDashboard from '@/components/dashboard/DynamicChildDashboard';
import {
Restaurant,
Hotel,
BabyChangingStation,
Insights,
SmartToy,
Analytics,
MedicalServices,
} from '@mui/icons-material';
import { motion } from 'framer-motion';
import { useAuth } from '@/lib/auth/AuthContext';
import { useRouter } from 'next/navigation';
import { childrenApi, Child } from '@/lib/api/children';
import { trackingApi } from '@/lib/api/tracking';
import { format, formatDistanceToNow } from 'date-fns';
import { useRealTimeActivities } from '@/hooks/useWebSocket';
import { useTranslation } from '@/hooks/useTranslation';
import { analyticsApi } from '@/lib/api/analytics';
import { useTheme } from '@mui/material/styles';
import { useDispatch, useSelector } from 'react-redux';
import { fetchChildren, selectSelectedChild } from '@/store/slices/childrenSlice';
import { AppDispatch, RootState } from '@/store/store';
export default function HomePage() {
const { t } = useTranslation('dashboard');
const theme = useTheme();
const { user, isLoading: authLoading } = useAuth();
const router = useRouter();
const dispatch = useDispatch<AppDispatch>();
const selectedChild = useSelector(selectSelectedChild);
const familyId = user?.families?.[0]?.familyId;
const [selectedChildId, setSelectedChildId] = useState<string | null>(null);
const [predictions, setPredictions] = useState<any>(null);
const [predictionsLoading, setPredictionsLoading] = useState(false);
const [children, setChildren] = useState<Child[]>([]);
const [recentActivities, setRecentActivities] = useState<any[]>([]);
const [todaySummary, setTodaySummary] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<any>(null);
// Load children
useEffect(() => {
if (familyId && !authLoading) {
loadChildren();
}
}, [familyId, authLoading]);
const loadChildren = async () => {
if (!familyId) return;
try {
const data = await childrenApi.getChildren(familyId);
setChildren(data);
if (data.length > 0 && !selectedChildId) {
setSelectedChildId(data[0].id);
}
} catch (err) {
console.error('Failed to load children:', err);
setError(err);
}
};
// Load dashboard data when child selected
useEffect(() => {
if (selectedChildId) {
loadDashboardData();
}
}, [selectedChildId]);
const loadDashboardData = async () => {
if (!selectedChildId) return;
setLoading(true);
try {
// Load recent activities
const activities = await trackingApi.getActivities(selectedChildId);
setRecentActivities(activities.slice(0, 10));
// Calculate today's summary from activities
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayActivities = activities.filter((a: any) => {
const activityDate = new Date(a.timestamp || a.startedAt);
return activityDate >= today;
});
const summary = {
feedingCount: todayActivities.filter((a: any) => a.type === 'feeding').length,
sleepCount: todayActivities.filter((a: any) => a.type === 'sleep').length,
diaperCount: todayActivities.filter((a: any) => a.type === 'diaper').length,
medicationCount: todayActivities.filter((a: any) => ['medication', 'medicine'].includes(a.type)).length,
totalFeedingAmount: todayActivities
.filter((a: any) => a.type === 'feeding')
.reduce((sum: number, a: any) => sum + (a.data?.amount || 0), 0),
totalSleepDuration: todayActivities
.filter((a: any) => a.type === 'sleep')
.reduce((sum: number, a: any) => {
const start = new Date(a.startedAt);
const end = a.endedAt ? new Date(a.endedAt) : new Date();
return sum + (end.getTime() - start.getTime()) / (1000 * 60);
}, 0),
};
setTodaySummary(summary);
setError(null);
} catch (err) {
console.error('Failed to load dashboard data:', err);
setError(err);
} finally {
setLoading(false);
}
};
const refetch = loadDashboardData;
// Real-time activity handler
const refreshDashboard = useCallback(async () => {
console.log('[HomePage] Refreshing dashboard data...');
await refetch();
}, [refetch]);
// Subscribe to real-time activity updates
useRealTimeActivities(
refreshDashboard, // On activity created
refreshDashboard, // On activity updated
refreshDashboard // On activity deleted
);
// Sync selectedChildId with Redux selectedChild
useEffect(() => {
if (selectedChild?.id && selectedChild.id !== selectedChildId) {
setSelectedChildId(selectedChild.id);
}
}, [selectedChild, selectedChildId]);
// Fetch predictions when selectedChildId changes
useEffect(() => {
const fetchPredictions = async () => {
if (!selectedChildId) return;
setPredictionsLoading(true);
try {
const predictionData = await analyticsApi.getPredictions(selectedChildId);
setPredictions(predictionData);
} catch (err) {
console.error('Failed to fetch predictions:', err);
setPredictions(null);
} finally {
setPredictionsLoading(false);
}
};
fetchPredictions();
}, [selectedChildId]);
const quickActions = [
{ icon: <Restaurant />, label: t('quickActions.feeding'), color: theme.palette.primary.main, path: '/track/feeding' },
{ icon: <Hotel />, label: t('quickActions.sleep'), color: theme.palette.secondary.main, path: '/track/sleep' },
{ icon: <BabyChangingStation />, label: t('quickActions.diaper'), color: theme.palette.warning.main, path: '/track/diaper' },
{ icon: <MedicalServices />, label: t('quickActions.medical'), color: theme.palette.error.main, path: '/track/medicine' },
{ icon: <Insights />, label: t('quickActions.activities'), color: theme.palette.success.main, path: '/track/activity' },
{ icon: <SmartToy />, label: t('quickActions.aiAssistant'), color: theme.palette.info.main, path: '/ai-assistant' },
];
const formatSleepHours = (minutes: number) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours > 0 && mins > 0) {
return `${hours}h ${mins}m`;
} else if (hours > 0) {
return `${hours}h`;
} else {
return `${mins}m`;
}
};
const isLoading = authLoading || loading;
// Build child metrics object for DynamicChildDashboard
const childMetrics = children.reduce((acc: any, child: any) => {
// Use todaySummary for selected child, or zero for others
if (child.id === selectedChildId && todaySummary) {
acc[child.id] = {
feedingCount: todaySummary.feedingCount || 0,
sleepDuration: todaySummary.totalSleepDuration || 0,
diaperCount: todaySummary.diaperCount || 0,
medicationCount: todaySummary.medicationCount || 0,
};
} else {
acc[child.id] = {
feedingCount: 0,
sleepDuration: 0,
diaperCount: 0,
medicationCount: 0,
};
}
return acc;
}, {});
const handleChildSelect = (childId: string) => {
setSelectedChildId(childId);
};
return (
<ProtectedRoute>
<AppShell>
<NetworkStatusIndicator />
<Box>
<EmailVerificationBanner />
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Typography variant="h4" component="h1" gutterBottom fontWeight="600" sx={{ mb: 1 }}>
{user?.name ? t('welcomeBackWithName', { name: user.name }) : t('welcomeBack')} 👋
</Typography>
<Typography variant="body1" sx={{ mb: 4, color: 'text.primary' }}>
{t('subtitle')}
</Typography>
{/* Quick Actions */}
<Typography variant="h6" component="h2" gutterBottom fontWeight="600" sx={{ mb: 2 }}>
{t('quickActions.title')}
</Typography>
<Grid container spacing={2} sx={{ mb: 4 }}>
{quickActions.map((action, index) => (
<Grid size={{ xs: 6, sm: 4, md: 2 }} key={action.label}>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
style={{ height: '100%' }}
>
<Paper
component="button"
onClick={() => router.push(action.path)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
router.push(action.path);
}
}}
aria-label={t('quickActions.navigateTo', { action: action.label })}
sx={{
p: 3,
height: '140px', // Fixed height for consistency
minHeight: '140px', // Ensure minimum height
width: '100%', // Full width of grid container
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
cursor: 'pointer',
bgcolor: action.color,
color: 'white',
border: 'none',
transition: 'transform 0.2s',
'&:hover': {
transform: 'scale(1.05)',
},
'&:focus-visible': {
outline: '3px solid white',
outlineOffset: '-3px',
transform: 'scale(1.05)',
},
}}
>
<Box sx={{ fontSize: 48, mb: 1 }} aria-hidden="true">{action.icon}</Box>
<Typography variant="body1" fontWeight="600">
{action.label}
</Typography>
</Paper>
</motion.div>
</Grid>
))}
</Grid>
{/* Today's Summary - Dynamic Multi-Child Dashboard */}
<Typography variant="h6" component="h2" gutterBottom fontWeight="600" sx={{ mb: 2 }}>
{t('summary.title')}
</Typography>
<ErrorBoundary
isolate
fallback={<DataErrorFallback error={new Error('Failed to load summary')} />}
>
{isLoading ? (
<StatGridSkeleton count={3} />
) : children.length === 0 ? (
<Paper sx={{ p: 3 }}>
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)' }}>
{t('summary.noChild')}
</Typography>
</Box>
</Paper>
) : (
<DynamicChildDashboard
childMetrics={childMetrics}
onChildSelect={handleChildSelect}
/>
)}
</ErrorBoundary>
{/* Next Predicted Activity */}
<Box sx={{ mt: 4 }}>
<Paper sx={{ p: 3, bgcolor: 'primary.light' }}>
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)' }} gutterBottom>
{t('predictions.title')}
</Typography>
{predictionsLoading ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<CircularProgress size={20} />
<Typography variant="body2">Loading predictions...</Typography>
</Box>
) : predictions?.sleep?.nextNapTime || predictions?.feeding?.nextFeedingTime ? (
<>
{predictions.sleep?.nextNapTime && (
<>
<Typography variant="h6" fontWeight="600" gutterBottom>
Nap time {formatDistanceToNow(new Date(predictions.sleep.nextNapTime), { addSuffix: true })}
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)' }}>
{predictions.sleep.reasoning || t('predictions.basedOnPatterns')}
</Typography>
</>
)}
{!predictions.sleep?.nextNapTime && predictions.feeding?.nextFeedingTime && (
<>
<Typography variant="h6" fontWeight="600" gutterBottom>
Feeding time {formatDistanceToNow(new Date(predictions.feeding.nextFeedingTime), { addSuffix: true })}
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(0, 0, 0, 0.7)' }}>
{predictions.feeding.reasoning || t('predictions.basedOnPatterns')}
</Typography>
</>
)}
</>
) : (
<Typography variant="body1" sx={{ color: 'rgba(0, 0, 0, 0.7)' }}>
{t('predictions.notEnoughData')}
</Typography>
)}
</Paper>
</Box>
</motion.div>
</Box>
</AppShell>
</ProtectedRoute>
);
}