feat: Add real data to families page and fix MUI Grid warnings
Some checks failed
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
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

Backend changes:
- Created FamiliesModule with controller and service
- Added /admin/families endpoint to list all families with members and children
- Added /admin/families/:id endpoint to get family details
- Added DELETE /admin/families/:id endpoint for family deletion
- Query families, family_members, children, and activities tables
- Calculate activity counts and last activity timestamps

Frontend changes:
- Removed all mock data from families page
- Connected to real /admin/families API endpoint
- Replaced deprecated MUI Grid v1 with CSS Grid layout
- Removed Grid import (no longer used)
- Fixed all Grid deprecation warnings (item, xs, sm, md props)
- Display real family data: members, children, activity counts
- Maintain responsive layout with CSS Grid breakpoints
This commit is contained in:
Andrei
2025-10-08 08:34:24 +00:00
parent 28a781517c
commit 0f56c68a1b
5 changed files with 309 additions and 234 deletions

View File

@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { UserManagementModule } from './user-management/user-management.module'; import { UserManagementModule } from './user-management/user-management.module';
import { DashboardModule } from './dashboard/dashboard.module'; import { DashboardModule } from './dashboard/dashboard.module';
import { FamiliesModule } from './families/families.module';
@Module({ @Module({
imports: [UserManagementModule, DashboardModule], imports: [UserManagementModule, DashboardModule, FamiliesModule],
exports: [UserManagementModule, DashboardModule], exports: [UserManagementModule, DashboardModule, FamiliesModule],
}) })
export class AdminModule {} export class AdminModule {}

View File

@@ -0,0 +1,24 @@
import { Controller, Get, Delete, Param, UseGuards } from '@nestjs/common';
import { FamiliesService } from './families.service';
import { AdminGuard } from '../../../common/guards/admin.guard';
@Controller('api/v1/admin/families')
@UseGuards(AdminGuard)
export class FamiliesController {
constructor(private readonly familiesService: FamiliesService) {}
@Get()
async listFamilies() {
return this.familiesService.listFamilies();
}
@Get(':id')
async getFamilyById(@Param('id') id: string) {
return this.familiesService.getFamilyById(id);
}
@Delete(':id')
async deleteFamily(@Param('id') id: string) {
return this.familiesService.deleteFamily(id);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FamiliesController } from './families.controller';
import { FamiliesService } from './families.service';
import { User } from '../../../database/entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [FamiliesController],
providers: [FamiliesService],
exports: [FamiliesService],
})
export class FamiliesModule {}

View File

@@ -0,0 +1,181 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../../../database/entities/user.entity';
@Injectable()
export class FamiliesService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async listFamilies() {
// Get all families with their members and children
const familiesRaw = await this.userRepository.query(`
SELECT
f.id,
f.name,
f.created_at as "createdAt",
COUNT(DISTINCT fm.user_id) as "memberCount",
COUNT(DISTINCT c.id) as "childrenCount",
COALESCE(act.activity_count, 0) as "activityCount",
COALESCE(act.last_activity_at, f.created_at) as "lastActivityAt"
FROM families f
LEFT JOIN family_members fm ON f.id = fm.family_id
LEFT JOIN children c ON f.id = c.family_id
LEFT JOIN LATERAL (
SELECT
COUNT(*) as activity_count,
MAX(created_at) as last_activity_at
FROM activities a
WHERE a.child_id IN (SELECT id FROM children WHERE family_id = f.id)
) act ON true
GROUP BY f.id, f.name, f.created_at, act.activity_count, act.last_activity_at
ORDER BY f.created_at DESC
`);
// Get members for each family
const families = await Promise.all(
familiesRaw.map(async (family: any) => {
const members = await this.userRepository.query(
`
SELECT
u.id,
u.name,
u.email,
fm.role,
fm.joined_at as "joinedAt"
FROM family_members fm
JOIN users u ON fm.user_id = u.id
WHERE fm.family_id = $1
ORDER BY fm.joined_at ASC
`,
[family.id],
);
const children = await this.userRepository.query(
`
SELECT
id,
name,
birth_date as "birthDate",
gender,
display_color as "displayColor"
FROM children
WHERE family_id = $1
ORDER BY birth_date DESC
`,
[family.id],
);
return {
...family,
memberCount: parseInt(family.memberCount, 10),
childrenCount: parseInt(family.childrenCount, 10),
activityCount: parseInt(family.activityCount, 10),
members,
children,
};
}),
);
return { data: families };
}
async getFamilyById(id: string) {
const familyRaw = await this.userRepository.query(
`
SELECT
f.id,
f.name,
f.created_at as "createdAt",
COUNT(DISTINCT fm.user_id) as "memberCount",
COUNT(DISTINCT c.id) as "childrenCount"
FROM families f
LEFT JOIN family_members fm ON f.id = fm.family_id
LEFT JOIN children c ON f.id = c.family_id
WHERE f.id = $1
GROUP BY f.id, f.name, f.created_at
`,
[id],
);
if (!familyRaw || familyRaw.length === 0) {
throw new NotFoundException(`Family with ID ${id} not found`);
}
const family = familyRaw[0];
const members = await this.userRepository.query(
`
SELECT
u.id,
u.name,
u.email,
fm.role,
fm.joined_at as "joinedAt"
FROM family_members fm
JOIN users u ON fm.user_id = u.id
WHERE fm.family_id = $1
ORDER BY fm.joined_at ASC
`,
[id],
);
const children = await this.userRepository.query(
`
SELECT
id,
name,
birth_date as "birthDate",
gender,
display_color as "displayColor"
FROM children
WHERE family_id = $1
ORDER BY birth_date DESC
`,
[id],
);
const activityStats = await this.userRepository.query(
`
SELECT
COUNT(*) as activity_count,
MAX(created_at) as last_activity_at
FROM activities
WHERE child_id IN (SELECT id FROM children WHERE family_id = $1)
`,
[id],
);
return {
...family,
memberCount: parseInt(family.memberCount, 10),
childrenCount: parseInt(family.childrenCount, 10),
activityCount: parseInt(activityStats[0]?.activity_count || '0', 10),
lastActivityAt: activityStats[0]?.last_activity_at || family.createdAt,
members,
children,
};
}
async deleteFamily(id: string) {
// Check if family exists
const family = await this.userRepository.query(
'SELECT id FROM families WHERE id = $1',
[id],
);
if (!family || family.length === 0) {
throw new NotFoundException(`Family with ID ${id} not found`);
}
// Delete family (cascading will handle members and children)
await this.userRepository.query('DELETE FROM families WHERE id = $1', [
id,
]);
return { message: 'Family deleted successfully' };
}
}

View File

@@ -25,7 +25,6 @@ import {
DialogActions, DialogActions,
Avatar, Avatar,
AvatarGroup, AvatarGroup,
Grid,
List, List,
ListItem, ListItem,
ListItemAvatar, ListItemAvatar,
@@ -90,142 +89,10 @@ export default function FamiliesPage() {
try { try {
setLoading(true); setLoading(true);
const response = await apiClient.get('/admin/families'); const response = await apiClient.get('/admin/families');
setFamilies(response.data); setFamilies(response.data || []);
} catch (error) { } catch (error) {
console.error('Failed to fetch families:', error); console.error('Failed to fetch families:', error);
// Using mock data for development setFamilies([]);
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 { } finally {
setLoading(false); setLoading(false);
} }
@@ -288,56 +155,48 @@ export default function FamiliesPage() {
</Box> </Box>
{/* Stats Cards */} {/* Stats Cards */}
<Grid container spacing={3} sx={{ mb: 4 }}> <Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: 3, mb: 4 }}>
<Grid item xs={12} sm={6} md={3}> <Card>
<Card> <CardContent>
<CardContent> <Typography color="text.secondary" gutterBottom>
<Typography color="text.secondary" gutterBottom> Total Families
Total Families </Typography>
</Typography> <Typography variant="h3" sx={{ color: 'primary.main' }}>
<Typography variant="h3" sx={{ color: 'primary.main' }}> {families.length}
{families.length} </Typography>
</Typography> </CardContent>
</CardContent> </Card>
</Card> <Card>
</Grid> <CardContent>
<Grid item xs={12} sm={6} md={3}> <Typography color="text.secondary" gutterBottom>
<Card> Total Members
<CardContent> </Typography>
<Typography color="text.secondary" gutterBottom> <Typography variant="h3" sx={{ color: 'success.main' }}>
Total Members {families.reduce((sum, f) => sum + f.memberCount, 0)}
</Typography> </Typography>
<Typography variant="h3" sx={{ color: 'success.main' }}> </CardContent>
{families.reduce((sum, f) => sum + f.memberCount, 0)} </Card>
</Typography> <Card>
</CardContent> <CardContent>
</Card> <Typography color="text.secondary" gutterBottom>
</Grid> Total Children
<Grid item xs={12} sm={6} md={3}> </Typography>
<Card> <Typography variant="h3" sx={{ color: 'info.main' }}>
<CardContent> {families.reduce((sum, f) => sum + f.childrenCount, 0)}
<Typography color="text.secondary" gutterBottom> </Typography>
Total Children </CardContent>
</Typography> </Card>
<Typography variant="h3" sx={{ color: 'info.main' }}> <Card>
{families.reduce((sum, f) => sum + f.childrenCount, 0)} <CardContent>
</Typography> <Typography color="text.secondary" gutterBottom>
</CardContent> Total Activities
</Card> </Typography>
</Grid> <Typography variant="h3" sx={{ color: 'secondary.main' }}>
<Grid item xs={12} sm={6} md={3}> {families.reduce((sum, f) => sum + f.activityCount, 0)}
<Card> </Typography>
<CardContent> </CardContent>
<Typography color="text.secondary" gutterBottom> </Card>
Total Activities </Box>
</Typography>
<Typography variant="h3" sx={{ color: 'secondary.main' }}>
{families.reduce((sum, f) => sum + f.activityCount, 0)}
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Search Bar */} {/* Search Bar */}
<Box sx={{ mb: 3 }}> <Box sx={{ mb: 3 }}>
@@ -464,52 +323,49 @@ export default function FamiliesPage() {
<DialogContent> <DialogContent>
{selectedFamily && ( {selectedFamily && (
<Box sx={{ pt: 2 }}> <Box sx={{ pt: 2 }}>
<Grid container spacing={3}> <Box sx={{ mb: 3 }}>
{/* Family Info */} <Typography variant="h6" gutterBottom>
<Grid item xs={12}> Family Information
<Typography variant="h6" gutterBottom> </Typography>
Family Information <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2 }}>
</Typography> <Box>
<Grid container spacing={2}> <Typography variant="subtitle2" color="text.secondary">
<Grid item xs={6}> Family ID
<Typography variant="subtitle2" color="text.secondary"> </Typography>
Family ID <Typography variant="body1">{selectedFamily.id}</Typography>
</Typography> </Box>
<Typography variant="body1">{selectedFamily.id}</Typography> <Box>
</Grid> <Typography variant="subtitle2" color="text.secondary">
<Grid item xs={6}> Created
<Typography variant="subtitle2" color="text.secondary"> </Typography>
Created <Typography variant="body1">
</Typography> {formatDate(selectedFamily.createdAt)}
<Typography variant="body1"> </Typography>
{formatDate(selectedFamily.createdAt)} </Box>
</Typography> <Box>
</Grid> <Typography variant="subtitle2" color="text.secondary">
<Grid item xs={6}> Total Activities
<Typography variant="subtitle2" color="text.secondary"> </Typography>
Total Activities <Typography variant="body1">
</Typography> {selectedFamily.activityCount}
<Typography variant="body1"> </Typography>
{selectedFamily.activityCount} </Box>
</Typography> <Box>
</Grid> <Typography variant="subtitle2" color="text.secondary">
<Grid item xs={6}> Last Activity
<Typography variant="subtitle2" color="text.secondary"> </Typography>
Last Activity <Typography variant="body1">
</Typography> {formatDate(selectedFamily.lastActivityAt)}
<Typography variant="body1"> </Typography>
{formatDate(selectedFamily.lastActivityAt)} </Box>
</Typography> </Box>
</Grid> </Box>
</Grid>
</Grid>
<Grid item xs={12}> <Divider sx={{ mb: 3 }} />
<Divider />
</Grid>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
{/* Members */} {/* Members */}
<Grid item xs={12} md={6}> <Box>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Members ({selectedFamily.memberCount}) Members ({selectedFamily.memberCount})
</Typography> </Typography>
@@ -539,10 +395,10 @@ export default function FamiliesPage() {
</ListItem> </ListItem>
))} ))}
</List> </List>
</Grid> </Box>
{/* Children */} {/* Children */}
<Grid item xs={12} md={6}> <Box>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Children ({selectedFamily.childrenCount}) Children ({selectedFamily.childrenCount})
</Typography> </Typography>
@@ -569,8 +425,8 @@ export default function FamiliesPage() {
</ListItem> </ListItem>
))} ))}
</List> </List>
</Grid> </Box>
</Grid> </Box>
</Box> </Box>
)} )}
</DialogContent> </DialogContent>