Files
maternal-app/maternal-web/app/children/page.tsx
Andrei 1b09a7d901
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
feat: Complete Phase 4 to 100% - All forms now have consistent error handling
Updated 4 additional pages to reach 100% Phase 4 completion:

1. Reset Password Page (auth/reset-password)
   - Added extractError() for password reset failures
   - Improved error messaging for expired tokens

2. Children Page (children/page)
   - Updated fetch, save, and delete operations
   - All 3 error handlers now use extractError()

3. Analytics Page (analytics/page)
   - Updated children loading, insights, and predictions
   - All 3 API calls now have consistent error handling

4. Advanced Analytics Page (analytics/advanced/page)
   - Updated 6 error handlers (children, circadian, anomalies, growth, correlations, trends)
   - Consistent error extraction across all analytics features

Phase 4 Status: 100% COMPLETE 
- Total forms updated: 21/21 (100%)
- Auth forms: 4/4 
- Family & child management: 3/3 
- Activity tracking: 6/6 
- Settings & onboarding: 2/2 
- Analytics & children pages: 4/4  (NEW)
- Other pages: 2/2  (PhotoUpload, components)

Error Improvement Plan: ~90% complete
- Phase 1-4: 100% 
- Phase 5-6: Backend improvements (pending)

All frontend forms now use centralized error handling with user-friendly,
multilingual error messages from the errorHandler utility.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 12:46:38 +00:00

316 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Grid,
Paper,
Button,
Avatar,
IconButton,
CircularProgress,
Alert,
Chip,
Container,
Card,
CardContent,
} from '@mui/material';
import { Add, ChildCare, Edit, Delete, CalendarToday } from '@mui/icons-material';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { useAuth } from '@/lib/auth/AuthContext';
import { childrenApi, Child, CreateChildData } from '@/lib/api/children';
import { ChildDialog } from '@/components/children/ChildDialog';
import { DeleteConfirmDialog } from '@/components/children/DeleteConfirmDialog';
import { motion } from 'framer-motion';
import { useTranslation } from '@/hooks/useTranslation';
import { useLocalizedDate } from '@/hooks/useLocalizedDate';
import { useSelectedFamily } from '@/hooks/useSelectedFamily';
import { extractError } from '@/lib/utils/errorHandler';
export default function ChildrenPage() {
const { t } = useTranslation('children');
const { user } = useAuth();
const { format: formatDate } = useLocalizedDate();
const { familyId, familyRole } = useSelectedFamily();
const [children, setChildren] = useState<Child[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
const [dialogOpen, setDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedChild, setSelectedChild] = useState<Child | null>(null);
const [childToDelete, setChildToDelete] = useState<Child | null>(null);
const [actionLoading, setActionLoading] = useState(false);
// Permission checks based on role
const canAddChildren = familyRole === 'parent';
const canEditChildren = familyRole === 'parent' || familyRole === 'caregiver';
const canDeleteChildren = familyRole === 'parent';
useEffect(() => {
if (familyId) {
fetchChildren();
} else {
setLoading(false);
setError(t('errors.noFamily'));
}
}, [familyId]);
const fetchChildren = async () => {
if (!familyId) return;
try {
setLoading(true);
setError('');
const data = await childrenApi.getChildren(familyId);
setChildren(data);
} catch (err: any) {
console.error('Failed to fetch children:', err);
const errorData = extractError(err);
setError(errorData.userMessage || errorData.message);
} finally {
setLoading(false);
}
};
const handleAddChild = () => {
setSelectedChild(null);
setDialogOpen(true);
};
const handleEditChild = (child: Child) => {
setSelectedChild(child);
setDialogOpen(true);
};
const handleDeleteClick = (child: Child) => {
setChildToDelete(child);
setDeleteDialogOpen(true);
};
const handleSubmit = async (data: CreateChildData) => {
if (!familyId) {
throw new Error(t('errors.noFamilyId'));
}
try {
setActionLoading(true);
if (selectedChild) {
await childrenApi.updateChild(selectedChild.id, data);
} else {
await childrenApi.createChild(familyId, data);
}
await fetchChildren();
setDialogOpen(false);
} catch (err: any) {
console.error('Failed to save child:', err);
const errorData = extractError(err);
throw new Error(errorData.userMessage || errorData.message);
} finally {
setActionLoading(false);
}
};
const handleDeleteConfirm = async () => {
if (!childToDelete) return;
try {
setActionLoading(true);
await childrenApi.deleteChild(childToDelete.id);
await fetchChildren();
setDeleteDialogOpen(false);
setChildToDelete(null);
} catch (err: any) {
console.error('Failed to delete child:', err);
const errorData = extractError(err);
setError(errorData.userMessage || errorData.message);
} finally {
setActionLoading(false);
}
};
const calculateAge = (birthDate: string): string => {
const birth = new Date(birthDate);
const today = new Date();
let years = today.getFullYear() - birth.getFullYear();
let months = today.getMonth() - birth.getMonth();
if (months < 0) {
years--;
months += 12;
}
if (today.getDate() < birth.getDate()) {
months--;
if (months < 0) {
years--;
months += 12;
}
}
if (years === 0) {
return `${months} ${months !== 1 ? t('ageFormat.months') : t('ageFormat.month')}`;
} else if (months === 0) {
return `${years} ${years !== 1 ? t('ageFormat.years') : t('ageFormat.year')}`;
} else {
return `${years} ${years !== 1 ? t('ageFormat.years') : t('ageFormat.year')}, ${months} ${months !== 1 ? t('ageFormat.months') : t('ageFormat.month')}`;
}
};
return (
<ProtectedRoute>
<AppShell>
<Container maxWidth="md" sx={{ px: { xs: 2, sm: 3 }, py: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
<Box>
<Typography variant="h4" component="h1" fontWeight="600">
{t('title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('subtitle')}
</Typography>
</Box>
{canAddChildren && (
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAddChild}
disabled={loading || !familyId}
sx={{
borderRadius: 2,
textTransform: 'none',
px: 3
}}
>
{t('addChild')}
</Button>
)}
</Box>
{error && (
<Alert severity="error" sx={{ mb: 3 }} onClose={() => setError('')}>
{error}
</Alert>
)}
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
) : children.length === 0 ? (
<Grid container spacing={3}>
<Grid item xs={12}>
<Card>
<CardContent sx={{ textAlign: 'center', py: 8 }}>
<ChildCare sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" component="h2" color="text.secondary" gutterBottom>
{t('noChildren')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('noChildrenSubtitle')}
</Typography>
{canAddChildren && (
<Button
variant="contained"
startIcon={<Add />}
onClick={handleAddChild}
disabled={!familyId}
>
{t('addFirstChild')}
</Button>
)}
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Grid container spacing={{ xs: 2, sm: 3 }}>
{children.map((child, index) => (
<Grid item xs={6} sm={6} md={4} key={child.id}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<Paper
elevation={0}
sx={{
p: { xs: 2, sm: 3 },
borderRadius: 3,
bgcolor: 'background.paper',
border: 1,
borderColor: 'divider'
}}
>
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, alignItems: { xs: 'center', sm: 'flex-start' }, gap: { xs: 1, sm: 2 } }}>
<Avatar
src={child.photoUrl}
alt={child.photoAlt || `Photo of ${child.name}`}
sx={{
width: { xs: 48, sm: 64 },
height: { xs: 48, sm: 64 },
bgcolor: 'primary.light',
fontSize: { xs: 20, sm: 24 }
}}
>
{child.name[0]}
</Avatar>
<Box sx={{ flexGrow: 1, textAlign: { xs: 'center', sm: 'left' }, width: '100%' }}>
<Typography variant="h6" fontWeight="600" sx={{ fontSize: { xs: '1rem', sm: '1.25rem' } }}>{child.name}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}>
{t(`gender.${child.gender}`)}
</Typography>
<Box sx={{ display: { xs: 'none', sm: 'flex' }, alignItems: 'center', gap: 1, mt: 1 }}>
<CalendarToday fontSize="small" color="action" />
<Typography variant="caption">
{formatDate(child.birthDate, 'PPP')}
</Typography>
</Box>
<Typography variant="caption" color="primary.main" sx={{ display: 'block', mt: 0.5 }}>
{t('age')}: {calculateAge(child.birthDate)}
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 1, mt: 2, justifyContent: { xs: 'center', sm: 'flex-start' } }}>
{canEditChildren && (
<IconButton size="medium" color="primary" onClick={() => handleEditChild(child)} sx={{ minWidth: 48, minHeight: 48 }}>
<Edit />
</IconButton>
)}
{canDeleteChildren && (
<IconButton size="medium" color="error" onClick={() => handleDeleteClick(child)} sx={{ minWidth: 48, minHeight: 48 }}>
<Delete />
</IconButton>
)}
</Box>
</Paper>
</motion.div>
</Grid>
))}
</Grid>
)}
</Container>
<ChildDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
onSubmit={handleSubmit}
child={selectedChild}
isLoading={actionLoading}
/>
<DeleteConfirmDialog
open={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
onConfirm={handleDeleteConfirm}
childName={childToDelete?.name || ''}
isLoading={actionLoading}
/>
</AppShell>
</ProtectedRoute>
);
}