import { Injectable, Logger, Inject } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { LicenseRequest } from '../../database/models/license-request.model'; import { Applicant } from '../../database/models/applicant.model'; import { Department } from '../../database/models/department.model'; import { User } from '../../database/models/user.model'; import { Document } from '../../database/models/document.model'; import { BlockchainTransaction } from '../../database/models/blockchain-transaction.model'; import { BlockchainEvent } from '../../database/models/blockchain-event.model'; import { ApplicationLog } from '../../database/models/application-log.model'; import { AuditLog } from '../../database/models/audit-log.model'; import { DepartmentsService } from '../departments/departments.service'; import { UsersService } from '../users/users.service'; export interface PlatformStats { totalRequests: number; requestsByStatus: Record; totalApplicants: number; activeApplicants: number; totalDepartments: number; activeDepartments: number; totalDocuments: number; totalBlockchainTransactions: number; transactionsByStatus: Record; } export interface SystemHealth { status: 'healthy' | 'degraded' | 'unhealthy'; uptime: number; timestamp: string; services: { database: { status: string }; blockchain: { status: string }; storage: { status: string }; queue: { status: string }; }; } @Injectable() export class AdminService { private readonly logger = new Logger(AdminService.name); constructor( @Inject(LicenseRequest) private requestModel: typeof LicenseRequest, @Inject(Applicant) private applicantModel: typeof Applicant, @Inject(Department) private departmentModel: typeof Department, @Inject(User) private userModel: typeof User, @Inject(Document) private documentModel: typeof Document, @Inject(BlockchainTransaction) private blockchainTxModel: typeof BlockchainTransaction, @Inject(BlockchainEvent) private blockchainEventModel: typeof BlockchainEvent, @Inject(ApplicationLog) private appLogModel: typeof ApplicationLog, @Inject(AuditLog) private auditLogModel: typeof AuditLog, private readonly configService: ConfigService, private readonly departmentsService: DepartmentsService, private readonly usersService: UsersService, ) {} async getPlatformStats(): Promise { this.logger.debug('Fetching platform statistics'); const [ totalRequests, requestsByStatus, totalApplicants, activeApplicants, totalDepartments, activeDepartments, totalDocuments, totalBlockchainTransactions, transactionsByStatus, ] = await Promise.all([ this.requestModel.query().resultSize(), this.requestModel .query() .select('status') .count('* as count') .groupBy('status') as any, this.applicantModel.query().resultSize(), this.applicantModel.query().where({ isActive: true }).resultSize(), this.departmentModel.query().resultSize(), this.departmentModel.query().where({ isActive: true }).resultSize(), this.documentModel.query().resultSize(), this.blockchainTxModel.query().resultSize(), this.blockchainTxModel .query() .select('status') .count('* as count') .groupBy('status') as any, ]); const statusMap: Record = {}; for (const row of requestsByStatus) { statusMap[(row as any).status] = parseInt((row as any).count, 10); } const txStatusMap: Record = {}; for (const row of transactionsByStatus) { txStatusMap[(row as any).status] = parseInt((row as any).count, 10); } return { totalRequests, requestsByStatus: statusMap, totalApplicants, activeApplicants, totalDepartments, activeDepartments, totalDocuments, totalBlockchainTransactions, transactionsByStatus: txStatusMap, }; } async getSystemHealth(): Promise { this.logger.debug('Checking system health'); let dbStatus = 'up'; try { await this.applicantModel.query().limit(1); } catch { dbStatus = 'down'; } const overallStatus = dbStatus === 'up' ? 'healthy' : 'unhealthy'; return { status: overallStatus, uptime: process.uptime(), timestamp: new Date().toISOString(), services: { database: { status: dbStatus }, blockchain: { status: 'up' }, storage: { status: 'up' }, queue: { status: 'up' }, }, }; } async getRecentActivity(limit: number = 20): Promise { return this.auditLogModel .query() .orderBy('created_at', 'DESC') .limit(limit); } async getBlockchainTransactions( page: number = 1, limit: number = 20, status?: string, ) { const query = this.blockchainTxModel.query().orderBy('created_at', 'DESC'); if (status) { query.where({ status }); } const offset = (page - 1) * limit; const [results, total] = await Promise.all([ query.clone().offset(offset).limit(limit), query.clone().resultSize(), ]); return { data: results, total, page, limit, totalPages: Math.ceil(total / limit), }; } async onboardDepartment(dto: any) { this.logger.debug(`Onboarding new department: ${dto.code}`); const result = await this.departmentsService.create(dto); return { department: result.department, apiKey: result.apiKey, apiSecret: result.apiSecret, message: 'Department onboarded successfully. Please save the API credentials as they will not be shown again.', }; } async getDepartments(page: number = 1, limit: number = 20) { return this.departmentsService.findAll({ page, limit }); } async getDepartment(id: string) { return this.departmentsService.findById(id); } async updateDepartment(id: string, dto: any) { return this.departmentsService.update(id, dto); } async regenerateDepartmentApiKey(id: string) { const result = await this.departmentsService.regenerateApiKey(id); return { ...result, message: 'API key regenerated successfully. Please save the new credentials as they will not be shown again.', }; } async deactivateDepartment(id: string) { await this.departmentsService.deactivate(id); return { message: 'Department deactivated successfully' }; } async activateDepartment(id: string) { const department = await this.departmentsService.activate(id); return { department, message: 'Department activated successfully' }; } async getUsers() { return this.usersService.findAll(); } async getBlockchainEvents( page: number = 1, limit: number = 20, eventType?: string, contractAddress?: string, ) { const query = this.blockchainEventModel .query() .orderBy('created_at', 'DESC'); if (eventType) { query.where({ eventType }); } if (contractAddress) { query.where({ contractAddress }); } const offset = (page - 1) * limit; const [results, total] = await Promise.all([ query.clone().offset(offset).limit(limit), query.clone().resultSize(), ]); return { data: results, total, page, limit, totalPages: Math.ceil(total / limit), }; } async getApplicationLogs( page: number = 1, limit: number = 50, level?: string, module?: string, search?: string, ) { const query = this.appLogModel .query() .orderBy('created_at', 'DESC'); if (level) { query.where({ level }); } if (module) { query.where('module', 'like', `%${module}%`); } if (search) { query.where('message', 'like', `%${search}%`); } const offset = (page - 1) * limit; const [results, total] = await Promise.all([ query.clone().offset(offset).limit(limit), query.clone().resultSize(), ]); return { data: results, total, page, limit, totalPages: Math.ceil(total / limit), }; } async getRequestDocuments(requestId: string) { this.logger.debug(`Fetching documents for request: ${requestId}`); // Fetch all documents for the request with related data const documents = await this.documentModel .query() .where({ requestId }) .withGraphFetched('[uploadedByUser, versions.[uploadedByUser], departmentReviews.[department]]') .orderBy('created_at', 'DESC'); // Transform documents to include formatted data return documents.map((doc: any) => ({ id: doc.id, name: doc.name, type: doc.type, size: doc.size, fileHash: doc.fileHash, ipfsHash: doc.ipfsHash, url: doc.url, thumbnailUrl: doc.thumbnailUrl, uploadedAt: doc.createdAt, uploadedBy: doc.uploadedByUser?.name || 'Unknown', currentVersion: doc.version || 1, versions: doc.versions?.map((v: any) => ({ id: v.id, version: v.version, fileHash: v.fileHash, uploadedAt: v.createdAt, uploadedBy: v.uploadedByUser?.name || 'Unknown', changes: v.changes, })) || [], departmentReviews: doc.departmentReviews?.map((review: any) => ({ departmentCode: review.department?.code || 'UNKNOWN', departmentName: review.department?.name || 'Unknown Department', reviewedAt: review.createdAt, reviewedBy: review.reviewedByUser?.name || 'Unknown', status: review.status, comments: review.comments, })) || [], metadata: { mimeType: doc.mimeType, width: doc.width, height: doc.height, pages: doc.pages, }, })); } }