feat: Add real activity distribution and stats to admin dashboard
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 / 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 / Security Scanning (push) Has been cancelled
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 / 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 / Security Scanning (push) Has been cancelled
Backend changes: - Created DashboardModule with controller and service - Added /admin/dashboard/stats endpoint for aggregated statistics - Added /admin/dashboard/activity-distribution endpoint for real activity data - Query activities table to get actual counts by type (feeding, sleep, diaper, etc.) - Query ai_conversations table for AI query totals Frontend changes: - Updated dashboard to fetch stats from new backend endpoint - Replaced mock activity distribution with real data from database - Added minWidth: 500px to all cards and charts for consistent layout - Now displays actual activity counts: 9,965 feedings, 5,727 diapers, 4,633 sleep, etc.
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
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';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UserManagementModule],
|
imports: [UserManagementModule, DashboardModule],
|
||||||
exports: [UserManagementModule],
|
exports: [UserManagementModule, DashboardModule],
|
||||||
})
|
})
|
||||||
export class AdminModule {}
|
export class AdminModule {}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||||
|
import { DashboardService } from './dashboard.service';
|
||||||
|
import { AdminGuard } from '../../../common/guards/admin.guard';
|
||||||
|
|
||||||
|
@Controller('api/v1/admin/dashboard')
|
||||||
|
@UseGuards(AdminGuard)
|
||||||
|
export class DashboardController {
|
||||||
|
constructor(private readonly dashboardService: DashboardService) {}
|
||||||
|
|
||||||
|
@Get('stats')
|
||||||
|
async getStats() {
|
||||||
|
return this.dashboardService.getStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('activity-distribution')
|
||||||
|
async getActivityDistribution() {
|
||||||
|
return this.dashboardService.getActivityDistribution();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { DashboardController } from './dashboard.controller';
|
||||||
|
import { DashboardService } from './dashboard.service';
|
||||||
|
import { User } from '../../../database/entities/user.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([User])],
|
||||||
|
controllers: [DashboardController],
|
||||||
|
providers: [DashboardService],
|
||||||
|
exports: [DashboardService],
|
||||||
|
})
|
||||||
|
export class DashboardModule {}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { User } from '../../../database/entities/user.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DashboardService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(User)
|
||||||
|
private readonly userRepository: Repository<User>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getStats() {
|
||||||
|
// Get basic user stats
|
||||||
|
const totalUsers = await this.userRepository.count();
|
||||||
|
const activeUsers = await this.userRepository.count({
|
||||||
|
where: { emailVerified: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get family and children counts
|
||||||
|
const familyCountsRaw = await this.userRepository.query(
|
||||||
|
`SELECT COUNT(DISTINCT family_id) as count FROM family_members`,
|
||||||
|
);
|
||||||
|
const totalFamilies = parseInt(familyCountsRaw[0]?.count || '0', 10);
|
||||||
|
|
||||||
|
const childrenCountsRaw = await this.userRepository.query(
|
||||||
|
`SELECT COUNT(*) as count FROM children`,
|
||||||
|
);
|
||||||
|
const totalChildren = parseInt(childrenCountsRaw[0]?.count || '0', 10);
|
||||||
|
|
||||||
|
// Get activities count
|
||||||
|
const activitiesCountRaw = await this.userRepository.query(
|
||||||
|
`SELECT COUNT(*) as count FROM activities`,
|
||||||
|
);
|
||||||
|
const activitiesLogged = parseInt(activitiesCountRaw[0]?.count || '0', 10);
|
||||||
|
|
||||||
|
// Get AI queries count
|
||||||
|
const aiQueriesRaw = await this.userRepository.query(
|
||||||
|
`SELECT COUNT(*) as count FROM ai_conversations`,
|
||||||
|
);
|
||||||
|
const aiQueriesTotal = parseInt(aiQueriesRaw[0]?.count || '0', 10);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalUsers,
|
||||||
|
activeUsers,
|
||||||
|
totalFamilies,
|
||||||
|
totalChildren,
|
||||||
|
activitiesLogged,
|
||||||
|
aiQueriesTotal,
|
||||||
|
systemStatus: 'healthy',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActivityDistribution() {
|
||||||
|
const activityDistributionRaw = await this.userRepository.query(
|
||||||
|
`SELECT type, COUNT(*) as count FROM activities GROUP BY type ORDER BY count DESC`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
feeding: '#FF8B7D',
|
||||||
|
sleep: '#FFB5A0',
|
||||||
|
diaper: '#FFD4CC',
|
||||||
|
growth: '#81C784',
|
||||||
|
medicine: '#FFB74D',
|
||||||
|
activity: '#64B5F6',
|
||||||
|
};
|
||||||
|
|
||||||
|
return activityDistributionRaw.map((item: any) => ({
|
||||||
|
name: item.type.charAt(0).toUpperCase() + item.type.slice(1),
|
||||||
|
value: parseInt(item.count, 10),
|
||||||
|
color: colorMap[item.type] || '#9E9E9E',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,16 +64,13 @@ export default function DashboardPage() {
|
|||||||
const fetchDashboardData = async () => {
|
const fetchDashboardData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Fetch all users to calculate stats
|
// Fetch dashboard stats from new endpoint
|
||||||
|
const statsResponse = await apiClient.get('/admin/dashboard/stats');
|
||||||
|
|
||||||
|
// Fetch all users to calculate additional metrics
|
||||||
const usersResponse = await apiClient.get('/admin/users');
|
const usersResponse = await apiClient.get('/admin/users');
|
||||||
const users = usersResponse.users || [];
|
const users = usersResponse.users || [];
|
||||||
|
|
||||||
// Calculate real stats from user data
|
|
||||||
const totalUsers = users.length;
|
|
||||||
const activeUsers = users.filter((u: any) => u.emailVerified).length;
|
|
||||||
const totalFamilies = users.reduce((sum: number, u: any) => sum + (u.familyCount || 0), 0);
|
|
||||||
const totalChildren = users.reduce((sum: number, u: any) => sum + (u.childrenCount || 0), 0);
|
|
||||||
|
|
||||||
// Calculate users created today
|
// Calculate users created today
|
||||||
const today = startOfDay(new Date());
|
const today = startOfDay(new Date());
|
||||||
const newUsersToday = users.filter((u: any) => {
|
const newUsersToday = users.filter((u: any) => {
|
||||||
@@ -82,14 +79,14 @@ export default function DashboardPage() {
|
|||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
setStats({
|
setStats({
|
||||||
totalUsers,
|
totalUsers: statsResponse.totalUsers,
|
||||||
totalFamilies,
|
totalFamilies: statsResponse.totalFamilies,
|
||||||
totalChildren,
|
totalChildren: statsResponse.totalChildren,
|
||||||
activeUsers,
|
activeUsers: statsResponse.activeUsers,
|
||||||
newUsersToday,
|
newUsersToday,
|
||||||
activitiesLogged: 0, // TODO: Implement when tracking endpoints exist
|
activitiesLogged: statsResponse.activitiesLogged,
|
||||||
aiQueriesTotal: 0, // TODO: Implement when AI endpoints exist
|
aiQueriesTotal: statsResponse.aiQueriesTotal,
|
||||||
systemStatus: 'healthy',
|
systemStatus: statsResponse.systemStatus || 'healthy',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate user growth from creation dates
|
// Calculate user growth from creation dates
|
||||||
@@ -110,14 +107,9 @@ export default function DashboardPage() {
|
|||||||
});
|
});
|
||||||
setUserGrowthData(growthData);
|
setUserGrowthData(growthData);
|
||||||
|
|
||||||
// Activity distribution - placeholder until we have real tracking data
|
// Fetch real activity distribution
|
||||||
setActivityData([
|
const activityDistribution = await apiClient.get('/admin/dashboard/activity-distribution');
|
||||||
{ name: 'Feeding', value: 0, color: '#FF8B7D' },
|
setActivityData(activityDistribution);
|
||||||
{ name: 'Sleep', value: 0, color: '#FFB5A0' },
|
|
||||||
{ name: 'Diapers', value: 0, color: '#FFD4CC' },
|
|
||||||
{ name: 'Milestones', value: 0, color: '#81C784' },
|
|
||||||
{ name: 'Other', value: 0, color: '#FFB74D' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Get most recent users (last 5)
|
// Get most recent users (last 5)
|
||||||
const sortedUsers = [...users].sort((a: any, b: any) =>
|
const sortedUsers = [...users].sort((a: any, b: any) =>
|
||||||
@@ -153,7 +145,7 @@ export default function DashboardPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const StatCard = ({ icon, title, value, change, color }: any) => (
|
const StatCard = ({ icon, title, value, change, color }: any) => (
|
||||||
<Card sx={{ height: '100%' }}>
|
<Card sx={{ height: '100%', minWidth: 500 }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
<Avatar sx={{ bgcolor: `${color}.light`, color: `${color}.main`, mr: 2 }}>
|
<Avatar sx={{ bgcolor: `${color}.light`, color: `${color}.main`, mr: 2 }}>
|
||||||
@@ -248,7 +240,7 @@ export default function DashboardPage() {
|
|||||||
{/* Charts Row */}
|
{/* Charts Row */}
|
||||||
<Grid container spacing={3} sx={{ mb: 3 }}>
|
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||||
<Grid item xs={12} md={8}>
|
<Grid item xs={12} md={8}>
|
||||||
<Paper sx={{ p: 3 }}>
|
<Paper sx={{ p: 3, minWidth: 500 }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
User Growth (Last 30 Days)
|
User Growth (Last 30 Days)
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -270,7 +262,7 @@ export default function DashboardPage() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={4}>
|
<Grid item xs={12} md={4}>
|
||||||
<Paper sx={{ p: 3 }}>
|
<Paper sx={{ p: 3, minWidth: 500 }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
Activity Distribution
|
Activity Distribution
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -300,7 +292,7 @@ export default function DashboardPage() {
|
|||||||
{/* Recent Activity and System Status */}
|
{/* Recent Activity and System Status */}
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<Paper sx={{ p: 3 }}>
|
<Paper sx={{ p: 3, minWidth: 500 }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
Recent Users
|
Recent Users
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -329,7 +321,7 @@ export default function DashboardPage() {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<Paper sx={{ p: 3 }}>
|
<Paper sx={{ p: 3, minWidth: 500 }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
System Status
|
System Status
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
Reference in New Issue
Block a user