Skip to content

Commit c279ba6

Browse files
authored
Merge pull request #324 from harouns-ux/feat/harouns-ux-doc-stats-websocket-notifications-admin-stats
feat: document stats, WebSocket gateway, notification entity, and admin stats
2 parents bb956bc + be1aa43 commit c279ba6

4 files changed

Lines changed: 207 additions & 0 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// [BE-52] Add admin dashboard statistics endpoint
2+
import { Controller, Get, UseGuards } from '@nestjs/common';
3+
import { InjectRepository } from '@nestjs/typeorm';
4+
import { Repository } from 'typeorm';
5+
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
6+
import { Document, DocumentStatus } from '../documents/entities/document.entity';
7+
import { User, UserRole } from '../users/entities/user.entity';
8+
9+
@Controller('admin/stats')
10+
@UseGuards(JwtAuthGuard)
11+
export class AdminStatsController {
12+
constructor(
13+
@InjectRepository(Document)
14+
private readonly documentRepo: Repository<Document>,
15+
@InjectRepository(User)
16+
private readonly userRepo: Repository<User>,
17+
) {}
18+
19+
@Get()
20+
async getDashboardStats() {
21+
const [
22+
totalUsers,
23+
totalDocuments,
24+
pendingDocuments,
25+
verifiedDocuments,
26+
flaggedDocuments,
27+
rejectedDocuments,
28+
] = await Promise.all([
29+
this.userRepo.count({ where: { role: UserRole.USER } }),
30+
this.documentRepo.count(),
31+
this.documentRepo.count({ where: { status: DocumentStatus.PENDING } }),
32+
this.documentRepo.count({ where: { status: DocumentStatus.VERIFIED } }),
33+
this.documentRepo.count({ where: { status: DocumentStatus.FLAGGED } }),
34+
this.documentRepo.count({ where: { status: DocumentStatus.REJECTED } }),
35+
]);
36+
37+
return {
38+
users: { total: totalUsers },
39+
documents: {
40+
total: totalDocuments,
41+
pending: pendingDocuments,
42+
verified: verifiedDocuments,
43+
flagged: flaggedDocuments,
44+
rejected: rejectedDocuments,
45+
},
46+
};
47+
}
48+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// [BE-49] Add document statistics endpoint
2+
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
3+
import { InjectRepository } from '@nestjs/typeorm';
4+
import { Repository } from 'typeorm';
5+
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
6+
import { Document, DocumentStatus } from '../documents/entities/document.entity';
7+
8+
@Controller('documents/stats')
9+
@UseGuards(JwtAuthGuard)
10+
export class DocumentStatsController {
11+
constructor(
12+
@InjectRepository(Document)
13+
private readonly documentRepo: Repository<Document>,
14+
) {}
15+
16+
@Get()
17+
async getOverallStats() {
18+
const total = await this.documentRepo.count();
19+
const byStatus = await this.documentRepo
20+
.createQueryBuilder('doc')
21+
.select('doc.status', 'status')
22+
.addSelect('COUNT(*)', 'count')
23+
.groupBy('doc.status')
24+
.getRawMany();
25+
26+
return { total, byStatus };
27+
}
28+
29+
@Get('owner/:ownerId')
30+
async getStatsByOwner(@Param('ownerId') ownerId: string) {
31+
const total = await this.documentRepo.count({ where: { ownerId } });
32+
const verified = await this.documentRepo.count({
33+
where: { ownerId, status: DocumentStatus.VERIFIED },
34+
});
35+
const flagged = await this.documentRepo.count({
36+
where: { ownerId, status: DocumentStatus.FLAGGED },
37+
});
38+
const pending = await this.documentRepo.count({
39+
where: { ownerId, status: DocumentStatus.PENDING },
40+
});
41+
42+
return { ownerId, total, verified, flagged, pending };
43+
}
44+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// [BE-51] Add in-app notification entity and endpoints
2+
import {
3+
Entity,
4+
PrimaryGeneratedColumn,
5+
Column,
6+
CreateDateColumn,
7+
ManyToOne,
8+
JoinColumn,
9+
Index,
10+
} from 'typeorm';
11+
import { User } from '../users/entities/user.entity';
12+
13+
export enum NotificationType {
14+
DOCUMENT_VERIFIED = 'document_verified',
15+
DOCUMENT_FLAGGED = 'document_flagged',
16+
DOCUMENT_REJECTED = 'document_rejected',
17+
PROCESSING_COMPLETE = 'processing_complete',
18+
SYSTEM = 'system',
19+
}
20+
21+
@Entity('notifications')
22+
@Index('IDX_NOTIFICATION_USER_ID', ['userId'])
23+
@Index('IDX_NOTIFICATION_IS_READ', ['isRead'])
24+
export class Notification {
25+
@PrimaryGeneratedColumn('uuid')
26+
id: string;
27+
28+
@Column({ name: 'user_id' })
29+
userId: string;
30+
31+
@ManyToOne(() => User, { onDelete: 'CASCADE' })
32+
@JoinColumn({ name: 'user_id' })
33+
user: User;
34+
35+
@Column({ type: 'enum', enum: NotificationType })
36+
type: NotificationType;
37+
38+
@Column()
39+
title: string;
40+
41+
@Column({ type: 'text' })
42+
message: string;
43+
44+
@Column({ name: 'is_read', default: false })
45+
isRead: boolean;
46+
47+
@Column({ name: 'document_id', nullable: true })
48+
documentId?: string;
49+
50+
@CreateDateColumn({ name: 'created_at' })
51+
createdAt: Date;
52+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// [BE-50] Add WebSocket gateway for real-time document processing updates
2+
import {
3+
WebSocketGateway,
4+
WebSocketServer,
5+
SubscribeMessage,
6+
MessageBody,
7+
ConnectedSocket,
8+
OnGatewayConnection,
9+
OnGatewayDisconnect,
10+
} from '@nestjs/websockets';
11+
import { Server, Socket } from 'socket.io';
12+
import { UseGuards } from '@nestjs/common';
13+
14+
export interface ProcessingUpdate {
15+
documentId: string;
16+
status: string;
17+
progress?: number;
18+
message?: string;
19+
}
20+
21+
@WebSocketGateway({ cors: { origin: '*' }, namespace: '/processing' })
22+
export class ProcessingGateway implements OnGatewayConnection, OnGatewayDisconnect {
23+
@WebSocketServer()
24+
server: Server;
25+
26+
handleConnection(client: Socket) {
27+
console.log(`Client connected: ${client.id}`);
28+
}
29+
30+
handleDisconnect(client: Socket) {
31+
console.log(`Client disconnected: ${client.id}`);
32+
}
33+
34+
@SubscribeMessage('subscribe')
35+
handleSubscribe(
36+
@MessageBody() documentId: string,
37+
@ConnectedSocket() client: Socket,
38+
) {
39+
client.join(`document:${documentId}`);
40+
return { event: 'subscribed', documentId };
41+
}
42+
43+
@SubscribeMessage('unsubscribe')
44+
handleUnsubscribe(
45+
@MessageBody() documentId: string,
46+
@ConnectedSocket() client: Socket,
47+
) {
48+
client.leave(`document:${documentId}`);
49+
return { event: 'unsubscribed', documentId };
50+
}
51+
52+
emitProcessingUpdate(update: ProcessingUpdate) {
53+
this.server
54+
.to(`document:${update.documentId}`)
55+
.emit('processing:update', update);
56+
}
57+
58+
emitProcessingComplete(documentId: string, status: string) {
59+
this.server
60+
.to(`document:${documentId}`)
61+
.emit('processing:complete', { documentId, status });
62+
}
63+
}

0 commit comments

Comments
 (0)