feat: Complete admin dashboard implementation
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

- Add comprehensive user management page with search, filters, and user details
- Create families management interface showing members and children
- Implement analytics dashboard with real-time charts using Recharts
- Add system health monitoring page with service status and metrics
- Create settings page with tabs for general, security, notifications, email, storage, and API settings
- All pages include mock data for development and are ready for API integration
This commit is contained in:
Andrei
2025-10-06 23:17:21 +00:00
parent 91b5923da1
commit cec6ceb97e
5 changed files with 2440 additions and 0 deletions

View File

@@ -0,0 +1,583 @@
'use client';
import { useState, useEffect } from 'react';
import {
Box,
Card,
CardContent,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Chip,
Button,
TextField,
InputAdornment,
TablePagination,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Avatar,
AvatarGroup,
Grid,
List,
ListItem,
ListItemAvatar,
ListItemText,
Divider,
} from '@mui/material';
import {
Search,
Edit,
Delete,
Visibility,
FamilyRestroom,
PersonAdd,
ChildCare,
Group,
} from '@mui/icons-material';
import AdminLayout from '@/components/AdminLayout';
import apiClient from '@/lib/api-client';
interface FamilyMember {
id: string;
name: string;
email: string;
role: 'parent' | 'caregiver';
joinedAt: string;
}
interface Child {
id: string;
name: string;
birthDate: string;
gender: 'male' | 'female' | 'other';
displayColor: string;
}
interface Family {
id: string;
name: string;
createdAt: string;
memberCount: number;
childrenCount: number;
activityCount: number;
lastActivityAt: string;
members: FamilyMember[];
children: Child[];
}
export default function FamiliesPage() {
const [families, setFamilies] = useState<Family[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [searchQuery, setSearchQuery] = useState('');
const [selectedFamily, setSelectedFamily] = useState<Family | null>(null);
const [viewDialogOpen, setViewDialogOpen] = useState(false);
useEffect(() => {
fetchFamilies();
}, []);
const fetchFamilies = async () => {
try {
setLoading(true);
const response = await apiClient.get('/admin/families');
setFamilies(response.data);
} catch (error) {
console.error('Failed to fetch families:', error);
// Using mock data for development
setFamilies([
{
id: '1',
name: 'The Doe Family',
createdAt: '2024-01-15T10:00:00Z',
memberCount: 2,
childrenCount: 2,
activityCount: 542,
lastActivityAt: '2024-10-06T08:30:00Z',
members: [
{
id: '1',
name: 'John Doe',
email: 'john.doe@example.com',
role: 'parent',
joinedAt: '2024-01-15T10:00:00Z',
},
{
id: '2',
name: 'Jane Doe',
email: 'jane.doe@example.com',
role: 'parent',
joinedAt: '2024-01-15T10:30:00Z',
},
],
children: [
{
id: '1',
name: 'Emma Doe',
birthDate: '2022-03-15',
gender: 'female',
displayColor: '#FFB5A0',
},
{
id: '2',
name: 'Liam Doe',
birthDate: '2020-08-22',
gender: 'male',
displayColor: '#81C784',
},
],
},
{
id: '2',
name: 'The Smith Family',
createdAt: '2024-02-20T14:30:00Z',
memberCount: 2,
childrenCount: 1,
activityCount: 287,
lastActivityAt: '2024-10-05T18:45:00Z',
members: [
{
id: '3',
name: 'Jane Smith',
email: 'jane.smith@example.com',
role: 'parent',
joinedAt: '2024-02-20T14:30:00Z',
},
{
id: '4',
name: 'Bob Smith',
email: 'bob.smith@example.com',
role: 'parent',
joinedAt: '2024-02-20T15:00:00Z',
},
],
children: [
{
id: '3',
name: 'Olivia Smith',
birthDate: '2023-01-10',
gender: 'female',
displayColor: '#FFD4CC',
},
],
},
{
id: '3',
name: 'The Johnson Family',
createdAt: '2024-03-10T09:15:00Z',
memberCount: 3,
childrenCount: 3,
activityCount: 892,
lastActivityAt: '2024-09-30T12:00:00Z',
members: [
{
id: '5',
name: 'Bob Johnson',
email: 'bob.johnson@example.com',
role: 'parent',
joinedAt: '2024-03-10T09:15:00Z',
},
{
id: '6',
name: 'Alice Johnson',
email: 'alice.johnson@example.com',
role: 'parent',
joinedAt: '2024-03-10T09:30:00Z',
},
{
id: '7',
name: 'Mary (Grandma)',
email: 'mary.johnson@example.com',
role: 'caregiver',
joinedAt: '2024-03-15T10:00:00Z',
},
],
children: [
{
id: '4',
name: 'Noah Johnson',
birthDate: '2021-05-20',
gender: 'male',
displayColor: '#64B5F6',
},
{
id: '5',
name: 'Sophia Johnson',
birthDate: '2019-11-08',
gender: 'female',
displayColor: '#BA68C8',
},
{
id: '6',
name: 'Ethan Johnson',
birthDate: '2023-07-15',
gender: 'male',
displayColor: '#FFB74D',
},
],
},
]);
} finally {
setLoading(false);
}
};
const handleViewFamily = (family: Family) => {
setSelectedFamily(family);
setViewDialogOpen(true);
};
const handleDeleteFamily = async (familyId: string) => {
if (window.confirm('Are you sure you want to delete this family? This action cannot be undone.')) {
try {
await apiClient.delete(`/admin/families/${familyId}`);
fetchFamilies();
} catch (error) {
console.error('Failed to delete family:', error);
}
}
};
const filteredFamilies = families.filter((family) =>
family.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
const calculateAge = (birthDate: string) => {
const birth = new Date(birthDate);
const now = new Date();
const ageInMonths = (now.getFullYear() - birth.getFullYear()) * 12 +
(now.getMonth() - birth.getMonth());
if (ageInMonths < 12) {
return `${ageInMonths} months`;
} else if (ageInMonths < 24) {
const months = ageInMonths % 12;
return months > 0 ? `1 year ${months} months` : '1 year';
} else {
const years = Math.floor(ageInMonths / 12);
return `${years} years`;
}
};
return (
<AdminLayout>
<Box sx={{ mb: 3 }}>
<Typography variant="h4" gutterBottom sx={{ fontWeight: 600 }}>
Family Management
</Typography>
<Typography variant="body1" color="text.secondary">
View and manage family groups, members, and children
</Typography>
</Box>
{/* Stats Cards */}
<Grid container spacing={3} sx={{ mb: 4 }}>
<Grid item xs={12} sm={6} md={3}>
<Card>
<CardContent>
<Typography color="text.secondary" gutterBottom>
Total Families
</Typography>
<Typography variant="h3" sx={{ color: 'primary.main' }}>
{families.length}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Card>
<CardContent>
<Typography color="text.secondary" gutterBottom>
Total Members
</Typography>
<Typography variant="h3" sx={{ color: 'success.main' }}>
{families.reduce((sum, f) => sum + f.memberCount, 0)}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Card>
<CardContent>
<Typography color="text.secondary" gutterBottom>
Total Children
</Typography>
<Typography variant="h3" sx={{ color: 'info.main' }}>
{families.reduce((sum, f) => sum + f.childrenCount, 0)}
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Card>
<CardContent>
<Typography color="text.secondary" gutterBottom>
Total Activities
</Typography>
<Typography variant="h3" sx={{ color: 'secondary.main' }}>
{families.reduce((sum, f) => sum + f.activityCount, 0)}
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Search Bar */}
<Box sx={{ mb: 3 }}>
<TextField
placeholder="Search families..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
}}
/>
</Box>
{/* Families Table */}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Family Name</TableCell>
<TableCell>Members</TableCell>
<TableCell>Children</TableCell>
<TableCell align="center">Activities</TableCell>
<TableCell>Created</TableCell>
<TableCell>Last Activity</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredFamilies
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((family) => (
<TableRow key={family.id}>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FamilyRestroom sx={{ color: 'primary.main' }} />
<Typography variant="body1" sx={{ fontWeight: 500 }}>
{family.name}
</Typography>
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<AvatarGroup max={3}>
{family.members.map((member) => (
<Avatar
key={member.id}
sx={{ bgcolor: 'primary.light', width: 32, height: 32 }}
>
{member.name.charAt(0)}
</Avatar>
))}
</AvatarGroup>
<Typography variant="body2">{family.memberCount}</Typography>
</Box>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<AvatarGroup max={3}>
{family.children.map((child) => (
<Avatar
key={child.id}
sx={{ bgcolor: child.displayColor, width: 32, height: 32 }}
>
{child.name.charAt(0)}
</Avatar>
))}
</AvatarGroup>
<Typography variant="body2">{family.childrenCount}</Typography>
</Box>
</TableCell>
<TableCell align="center">
<Chip label={family.activityCount} size="small" />
</TableCell>
<TableCell>{formatDate(family.createdAt)}</TableCell>
<TableCell>{formatDate(family.lastActivityAt)}</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => handleViewFamily(family)}
title="View Details"
>
<Visibility />
</IconButton>
<IconButton
size="small"
onClick={() => handleDeleteFamily(family.id)}
title="Delete"
color="error"
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={filteredFamilies.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={(e, newPage) => setPage(newPage)}
onRowsPerPageChange={(e) => {
setRowsPerPage(parseInt(e.target.value, 10));
setPage(0);
}}
/>
</TableContainer>
{/* View Family Dialog */}
<Dialog
open={viewDialogOpen}
onClose={() => setViewDialogOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>{selectedFamily?.name}</DialogTitle>
<DialogContent>
{selectedFamily && (
<Box sx={{ pt: 2 }}>
<Grid container spacing={3}>
{/* Family Info */}
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
Family Information
</Typography>
<Grid container spacing={2}>
<Grid item xs={6}>
<Typography variant="subtitle2" color="text.secondary">
Family ID
</Typography>
<Typography variant="body1">{selectedFamily.id}</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="subtitle2" color="text.secondary">
Created
</Typography>
<Typography variant="body1">
{formatDate(selectedFamily.createdAt)}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="subtitle2" color="text.secondary">
Total Activities
</Typography>
<Typography variant="body1">
{selectedFamily.activityCount}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="subtitle2" color="text.secondary">
Last Activity
</Typography>
<Typography variant="body1">
{formatDate(selectedFamily.lastActivityAt)}
</Typography>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
{/* Members */}
<Grid item xs={12} md={6}>
<Typography variant="h6" gutterBottom>
Members ({selectedFamily.memberCount})
</Typography>
<List>
{selectedFamily.members.map((member) => (
<ListItem key={member.id} sx={{ px: 0 }}>
<ListItemAvatar>
<Avatar sx={{ bgcolor: 'primary.main' }}>
{member.name.charAt(0)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={member.name}
secondary={
<>
{member.email}
<br />
<Chip
label={member.role}
size="small"
sx={{ mt: 0.5 }}
color={member.role === 'parent' ? 'primary' : 'default'}
/>
</>
}
/>
</ListItem>
))}
</List>
</Grid>
{/* Children */}
<Grid item xs={12} md={6}>
<Typography variant="h6" gutterBottom>
Children ({selectedFamily.childrenCount})
</Typography>
<List>
{selectedFamily.children.map((child) => (
<ListItem key={child.id} sx={{ px: 0 }}>
<ListItemAvatar>
<Avatar sx={{ bgcolor: child.displayColor }}>
{child.name.charAt(0)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={child.name}
secondary={
<>
Age: {calculateAge(child.birthDate)}
<br />
Gender: {child.gender}
<br />
Born: {formatDate(child.birthDate)}
</>
}
/>
</ListItem>
))}
</List>
</Grid>
</Grid>
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setViewDialogOpen(false)}>Close</Button>
</DialogActions>
</Dialog>
</AdminLayout>
);
}