From 0f56c68a1bdda9d703af75c3bddf4cae094310d1 Mon Sep 17 00:00:00 2001 From: Andrei Date: Wed, 8 Oct 2025 08:34:24 +0000 Subject: [PATCH] feat: Add real data to families page and fix MUI Grid warnings 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 --- .../src/modules/admin/admin.module.ts | 5 +- .../admin/families/families.controller.ts | 24 ++ .../modules/admin/families/families.module.ts | 13 + .../admin/families/families.service.ts | 181 ++++++++++ parentflow-admin/src/app/families/page.tsx | 320 +++++------------- 5 files changed, 309 insertions(+), 234 deletions(-) create mode 100644 maternal-app/maternal-app-backend/src/modules/admin/families/families.controller.ts create mode 100644 maternal-app/maternal-app-backend/src/modules/admin/families/families.module.ts create mode 100644 maternal-app/maternal-app-backend/src/modules/admin/families/families.service.ts diff --git a/maternal-app/maternal-app-backend/src/modules/admin/admin.module.ts b/maternal-app/maternal-app-backend/src/modules/admin/admin.module.ts index 5f511d4..f56c6d3 100644 --- a/maternal-app/maternal-app-backend/src/modules/admin/admin.module.ts +++ b/maternal-app/maternal-app-backend/src/modules/admin/admin.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { UserManagementModule } from './user-management/user-management.module'; import { DashboardModule } from './dashboard/dashboard.module'; +import { FamiliesModule } from './families/families.module'; @Module({ - imports: [UserManagementModule, DashboardModule], - exports: [UserManagementModule, DashboardModule], + imports: [UserManagementModule, DashboardModule, FamiliesModule], + exports: [UserManagementModule, DashboardModule, FamiliesModule], }) export class AdminModule {} diff --git a/maternal-app/maternal-app-backend/src/modules/admin/families/families.controller.ts b/maternal-app/maternal-app-backend/src/modules/admin/families/families.controller.ts new file mode 100644 index 0000000..23d64f9 --- /dev/null +++ b/maternal-app/maternal-app-backend/src/modules/admin/families/families.controller.ts @@ -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); + } +} diff --git a/maternal-app/maternal-app-backend/src/modules/admin/families/families.module.ts b/maternal-app/maternal-app-backend/src/modules/admin/families/families.module.ts new file mode 100644 index 0000000..d88a539 --- /dev/null +++ b/maternal-app/maternal-app-backend/src/modules/admin/families/families.module.ts @@ -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 {} diff --git a/maternal-app/maternal-app-backend/src/modules/admin/families/families.service.ts b/maternal-app/maternal-app-backend/src/modules/admin/families/families.service.ts new file mode 100644 index 0000000..2b3fd16 --- /dev/null +++ b/maternal-app/maternal-app-backend/src/modules/admin/families/families.service.ts @@ -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, + ) {} + + 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' }; + } +} diff --git a/parentflow-admin/src/app/families/page.tsx b/parentflow-admin/src/app/families/page.tsx index 0543e97..747c6a6 100644 --- a/parentflow-admin/src/app/families/page.tsx +++ b/parentflow-admin/src/app/families/page.tsx @@ -25,7 +25,6 @@ import { DialogActions, Avatar, AvatarGroup, - Grid, List, ListItem, ListItemAvatar, @@ -90,142 +89,10 @@ export default function FamiliesPage() { try { setLoading(true); const response = await apiClient.get('/admin/families'); - setFamilies(response.data); + 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', - }, - ], - }, - ]); + setFamilies([]); } finally { setLoading(false); } @@ -288,56 +155,48 @@ export default function FamiliesPage() { {/* Stats Cards */} - - - - - - Total Families - - - {families.length} - - - - - - - - - Total Members - - - {families.reduce((sum, f) => sum + f.memberCount, 0)} - - - - - - - - - Total Children - - - {families.reduce((sum, f) => sum + f.childrenCount, 0)} - - - - - - - - - Total Activities - - - {families.reduce((sum, f) => sum + f.activityCount, 0)} - - - - - + + + + + Total Families + + + {families.length} + + + + + + + Total Members + + + {families.reduce((sum, f) => sum + f.memberCount, 0)} + + + + + + + Total Children + + + {families.reduce((sum, f) => sum + f.childrenCount, 0)} + + + + + + + Total Activities + + + {families.reduce((sum, f) => sum + f.activityCount, 0)} + + + + {/* Search Bar */} @@ -464,52 +323,49 @@ export default function FamiliesPage() { {selectedFamily && ( - - {/* Family Info */} - - - Family Information - - - - - Family ID - - {selectedFamily.id} - - - - Created - - - {formatDate(selectedFamily.createdAt)} - - - - - Total Activities - - - {selectedFamily.activityCount} - - - - - Last Activity - - - {formatDate(selectedFamily.lastActivityAt)} - - - - + + + Family Information + + + + + Family ID + + {selectedFamily.id} + + + + Created + + + {formatDate(selectedFamily.createdAt)} + + + + + Total Activities + + + {selectedFamily.activityCount} + + + + + Last Activity + + + {formatDate(selectedFamily.lastActivityAt)} + + + + - - - + + {/* Members */} - + Members ({selectedFamily.memberCount}) @@ -539,10 +395,10 @@ export default function FamiliesPage() { ))} - + {/* Children */} - + Children ({selectedFamily.childrenCount}) @@ -569,8 +425,8 @@ export default function FamiliesPage() { ))} - - + + )}