admin area
This commit is contained in:
180
apps/api/src/domain/admin/use-cases/GetDashboardStatsUseCase.ts
Normal file
180
apps/api/src/domain/admin/use-cases/GetDashboardStatsUseCase.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { IAdminUserRepository } from '@core/admin/domain/repositories/IAdminUserRepository';
|
||||
import { AuthorizationService } from '@core/admin/domain/services/AuthorizationService';
|
||||
import { UserId } from '@core/admin/domain/value-objects/UserId';
|
||||
|
||||
export interface DashboardStatsResult {
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
suspendedUsers: number;
|
||||
deletedUsers: number;
|
||||
systemAdmins: number;
|
||||
recentLogins: number;
|
||||
newUsersToday: number;
|
||||
userGrowth: {
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}[];
|
||||
roleDistribution: {
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}[];
|
||||
statusDistribution: {
|
||||
active: number;
|
||||
suspended: number;
|
||||
deleted: number;
|
||||
};
|
||||
activityTimeline: {
|
||||
date: string;
|
||||
newUsers: number;
|
||||
logins: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type GetDashboardStatsInput = {
|
||||
actorId: string;
|
||||
};
|
||||
|
||||
export type GetDashboardStatsErrorCode = 'AUTHORIZATION_ERROR' | 'REPOSITORY_ERROR';
|
||||
|
||||
export type GetDashboardStatsApplicationError = ApplicationErrorCode<GetDashboardStatsErrorCode, { message: string; details?: unknown }>;
|
||||
|
||||
export class GetDashboardStatsUseCase {
|
||||
constructor(
|
||||
private readonly adminUserRepo: IAdminUserRepository,
|
||||
private readonly output: UseCaseOutputPort<DashboardStatsResult>,
|
||||
) {}
|
||||
|
||||
async execute(input: GetDashboardStatsInput): Promise<Result<void, GetDashboardStatsApplicationError>> {
|
||||
try {
|
||||
// Get actor (current user)
|
||||
const actor = await this.adminUserRepo.findById(UserId.fromString(input.actorId));
|
||||
if (!actor) {
|
||||
return Result.err({
|
||||
code: 'AUTHORIZATION_ERROR',
|
||||
details: { message: 'Actor not found' },
|
||||
});
|
||||
}
|
||||
|
||||
// Check authorization
|
||||
if (!AuthorizationService.canListUsers(actor)) {
|
||||
return Result.err({
|
||||
code: 'AUTHORIZATION_ERROR',
|
||||
details: { message: 'User is not authorized to view dashboard' },
|
||||
});
|
||||
}
|
||||
|
||||
// Get all users
|
||||
const allUsersResult = await this.adminUserRepo.list();
|
||||
const allUsers = allUsersResult.users;
|
||||
|
||||
// Calculate basic stats
|
||||
const totalUsers = allUsers.length;
|
||||
const activeUsers = allUsers.filter(u => u.status.value === 'active').length;
|
||||
const suspendedUsers = allUsers.filter(u => u.status.value === 'suspended').length;
|
||||
const deletedUsers = allUsers.filter(u => u.status.value === 'deleted').length;
|
||||
const systemAdmins = allUsers.filter(u => u.isSystemAdmin()).length;
|
||||
|
||||
// Recent logins (last 24 hours)
|
||||
const oneDayAgo = new Date();
|
||||
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
|
||||
const recentLogins = allUsers.filter(u => u.lastLoginAt && u.lastLoginAt > oneDayAgo).length;
|
||||
|
||||
// New users today
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const newUsersToday = allUsers.filter(u => u.createdAt > today).length;
|
||||
|
||||
// Role distribution
|
||||
const roleCounts: Record<string, number> = {};
|
||||
allUsers.forEach(user => {
|
||||
user.roles.forEach(role => {
|
||||
const roleValue = role.value;
|
||||
roleCounts[roleValue] = (roleCounts[roleValue] || 0) + 1;
|
||||
});
|
||||
});
|
||||
|
||||
const roleDistribution = Object.entries(roleCounts).map(([role, count]) => ({
|
||||
label: role.charAt(0).toUpperCase() + role.slice(1),
|
||||
value: count,
|
||||
color: role === 'owner' ? 'text-purple-500' : role === 'admin' ? 'text-blue-500' : 'text-gray-500',
|
||||
}));
|
||||
|
||||
// User growth (last 7 days)
|
||||
const userGrowth: DashboardStatsResult['userGrowth'] = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
|
||||
const count = allUsers.filter(u => {
|
||||
const userDate = new Date(u.createdAt);
|
||||
return userDate.toDateString() === date.toDateString();
|
||||
}).length;
|
||||
|
||||
userGrowth.push({
|
||||
label: dateStr,
|
||||
value: count,
|
||||
color: 'text-primary-blue',
|
||||
});
|
||||
}
|
||||
|
||||
// Activity timeline (last 7 days)
|
||||
const activityTimeline: DashboardStatsResult['activityTimeline'] = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
|
||||
const newUsers = allUsers.filter(u => {
|
||||
const userDate = new Date(u.createdAt);
|
||||
return userDate.toDateString() === date.toDateString();
|
||||
}).length;
|
||||
|
||||
const logins = allUsers.filter(u => {
|
||||
const loginDate = u.lastLoginAt;
|
||||
return loginDate && loginDate.toDateString() === date.toDateString();
|
||||
}).length;
|
||||
|
||||
activityTimeline.push({
|
||||
date: dateStr,
|
||||
newUsers,
|
||||
logins,
|
||||
});
|
||||
}
|
||||
|
||||
const result: DashboardStatsResult = {
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
suspendedUsers,
|
||||
deletedUsers,
|
||||
systemAdmins,
|
||||
recentLogins,
|
||||
newUsersToday,
|
||||
userGrowth,
|
||||
roleDistribution,
|
||||
statusDistribution: {
|
||||
active: activeUsers,
|
||||
suspended: suspendedUsers,
|
||||
deleted: deletedUsers,
|
||||
},
|
||||
activityTimeline,
|
||||
};
|
||||
|
||||
this.output.present(result);
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to get dashboard stats';
|
||||
|
||||
return Result.err({
|
||||
code: 'REPOSITORY_ERROR',
|
||||
details: { message },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user