Skip to content

Commit 4bea8ce

Browse files
Merge pull request #1057 from walexjnr/feat/reports-websocket-924-925-927-928
feat: add reports module and WebSocket notifications gateway
2 parents 78941e7 + 671a9ce commit 4bea8ce

10 files changed

Lines changed: 397 additions & 0 deletions

backend/src/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { ContractsModule } from './contracts/contracts.module';
2626
import { LicensesModule } from './licenses/licenses.module';
2727
import { PurchaseOrdersModule } from './purchase-orders/purchase-orders.module';
2828
import { TasksModule } from './tasks/tasks.module';
29+
import { ReportsModule } from './reports/reports.module';
30+
import { NotificationsModule } from './notifications/notifications.module';
2931
import { ApiKeysModule } from './api-keys/api-keys.module';
3032
import { StellarModule } from './stellar/stellar.module';
3133
import { NotificationModule } from './notifications/notification.module';
@@ -110,6 +112,8 @@ import { NotificationModule } from './notifications/notification.module';
110112
InventoryModule,
111113
VendorsModule,
112114
DashboardModule,
115+
ReportsModule,
116+
NotificationsModule,
113117
ApiKeysModule,
114118
StellarModule,
115119
NotificationModule,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {
2+
Entity,
3+
PrimaryGeneratedColumn,
4+
Column,
5+
CreateDateColumn,
6+
} from 'typeorm';
7+
8+
@Entity('notifications')
9+
export class Notification {
10+
@PrimaryGeneratedColumn('uuid')
11+
id: string;
12+
13+
@Column()
14+
userId: string;
15+
16+
@Column()
17+
type: string;
18+
19+
@Column('text')
20+
message: string;
21+
22+
@Column({ default: false })
23+
read: boolean;
24+
25+
@CreateDateColumn()
26+
createdAt: Date;
27+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Controller, Get, Patch, Param, Req, UseGuards } from '@nestjs/common';
2+
import { AuthGuard } from '@nestjs/passport';
3+
import { NotificationsService } from './notifications.service';
4+
5+
@Controller('notifications')
6+
@UseGuards(AuthGuard('jwt'))
7+
export class NotificationsController {
8+
constructor(private readonly notificationsService: NotificationsService) {}
9+
10+
@Get()
11+
findAll(@Req() req: any) {
12+
return this.notificationsService.findByUser(req.user?.id);
13+
}
14+
15+
@Patch(':id/read')
16+
markRead(@Param('id') id: string, @Req() req: any) {
17+
return this.notificationsService.markRead(id, req.user?.id);
18+
}
19+
20+
@Patch('read-all')
21+
markAllRead(@Req() req: any) {
22+
return this.notificationsService.markAllRead(req.user?.id);
23+
}
24+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
WebSocketGateway,
3+
WebSocketServer,
4+
SubscribeMessage,
5+
OnGatewayConnection,
6+
OnGatewayDisconnect,
7+
} from '@nestjs/websockets';
8+
import { Server, Socket } from 'socket.io';
9+
import { JwtService } from '@nestjs/jwt';
10+
import { Injectable } from '@nestjs/common';
11+
12+
@Injectable()
13+
@WebSocketGateway({ namespace: '/notifications', cors: { origin: '*' } })
14+
export class NotificationsGateway
15+
implements OnGatewayConnection, OnGatewayDisconnect
16+
{
17+
@WebSocketServer()
18+
server: Server;
19+
20+
constructor(private readonly jwtService: JwtService) {}
21+
22+
handleConnection(client: Socket) {
23+
try {
24+
const token = (client.handshake.auth?.token ||
25+
client.handshake.query?.token) as string;
26+
const payload = this.jwtService.verify(token);
27+
client.data.userId = payload.sub;
28+
void client.join(`user:${String(payload.sub)}`);
29+
} catch {
30+
client.disconnect();
31+
}
32+
}
33+
34+
handleDisconnect(_client: Socket) {}
35+
36+
sendToUser(userId: string, event: string, data: unknown) {
37+
this.server.to(`user:${userId}`).emit(event, data);
38+
}
39+
40+
@SubscribeMessage('ping')
41+
handlePing() {
42+
return { event: 'pong', data: {} };
43+
}
44+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { JwtModule } from '@nestjs/jwt';
4+
import { ConfigModule, ConfigService } from '@nestjs/config';
5+
import { Notification } from './entities/notification.entity';
6+
import { NotificationsGateway } from './notifications.gateway';
7+
import { NotificationsService } from './notifications.service';
8+
import { NotificationsController } from './notifications.controller';
9+
10+
@Module({
11+
imports: [
12+
TypeOrmModule.forFeature([Notification]),
13+
JwtModule.registerAsync({
14+
imports: [ConfigModule],
15+
inject: [ConfigService],
16+
useFactory: (config: ConfigService) => ({
17+
secret: config.get('JWT_SECRET', 'change-me-in-env'),
18+
signOptions: { expiresIn: '15m' },
19+
}),
20+
}),
21+
ConfigModule,
22+
],
23+
controllers: [NotificationsController],
24+
providers: [NotificationsGateway, NotificationsService],
25+
exports: [NotificationsService, NotificationsGateway],
26+
})
27+
export class NotificationsModule {}

backend/src/notifications/notifications.service.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,36 @@ export class NotificationsService {
5656
where: { userId, isRead: false },
5757
});
5858
}
59+
60+
async sendToUser(
61+
userId: string,
62+
type: string,
63+
message: string,
64+
): Promise<Notification> {
65+
return this.create({
66+
userId,
67+
event: type as any,
68+
title: type,
69+
message,
70+
emailTemplate: '',
71+
emailSubject: '',
72+
emailContext: {},
73+
} as any);
74+
}
75+
76+
async findByUser(userId: string): Promise<Notification[]> {
77+
return this.findByUserId(userId);
78+
}
79+
80+
async markRead(id: string, userId: string): Promise<Notification | null> {
81+
const notification = await this.notificationRepository.findOne({
82+
where: { id, userId },
83+
});
84+
if (!notification) return null;
85+
return this.markAsRead(id);
86+
}
87+
88+
async markAllRead(userId: string): Promise<void> {
89+
return this.markAllAsRead(userId);
90+
}
5991
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
Entity,
3+
PrimaryGeneratedColumn,
4+
Column,
5+
CreateDateColumn,
6+
UpdateDateColumn,
7+
} from 'typeorm';
8+
9+
@Entity('report_schedules')
10+
export class ReportSchedule {
11+
@PrimaryGeneratedColumn('uuid')
12+
id: string;
13+
14+
@Column()
15+
userId: string;
16+
17+
@Column()
18+
reportType: string;
19+
20+
@Column()
21+
frequency: string;
22+
23+
@Column()
24+
email: string;
25+
26+
@Column({ default: true })
27+
isActive: boolean;
28+
29+
@Column({ nullable: true, type: 'timestamp' })
30+
lastRunAt: Date;
31+
32+
@Column({ nullable: true, type: 'timestamp' })
33+
nextRunAt: Date;
34+
35+
@CreateDateColumn()
36+
createdAt: Date;
37+
38+
@UpdateDateColumn()
39+
updatedAt: Date;
40+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
Controller,
3+
Get,
4+
Post,
5+
Delete,
6+
Query,
7+
Body,
8+
Param,
9+
Req,
10+
UseGuards,
11+
} from '@nestjs/common';
12+
import { AuthGuard } from '@nestjs/passport';
13+
import { ReportsService } from './reports.service';
14+
15+
@Controller('reports')
16+
@UseGuards(AuthGuard('jwt'))
17+
export class ReportsController {
18+
constructor(private readonly reportsService: ReportsService) {}
19+
20+
@Get('summary')
21+
getSummary() {
22+
return this.reportsService.getSummary();
23+
}
24+
25+
@Get('warranty-expiring')
26+
getWarrantyExpiring(@Query('days') days?: string) {
27+
return this.reportsService.getWarrantyExpiring(
28+
days ? parseInt(days, 10) : 30,
29+
);
30+
}
31+
32+
@Get('maintenance-costs')
33+
getMaintenanceCosts() {
34+
return this.reportsService.getMaintenanceCosts();
35+
}
36+
37+
@Get('depreciation')
38+
getDepreciation() {
39+
return this.reportsService.getDepreciation();
40+
}
41+
42+
@Get('asset-utilisation')
43+
getAssetUtilisation() {
44+
return this.reportsService.getAssetUtilisation();
45+
}
46+
47+
@Get('schedules')
48+
getSchedules(@Req() req: any) {
49+
return this.reportsService.getSchedules(req.user?.id);
50+
}
51+
52+
@Post('schedules')
53+
createSchedule(
54+
@Req() req: any,
55+
@Body() body: { reportType: string; frequency: string; email: string },
56+
) {
57+
return this.reportsService.createSchedule(
58+
req.user?.id,
59+
body.reportType,
60+
body.frequency,
61+
body.email,
62+
);
63+
}
64+
65+
@Delete('schedules/:id')
66+
deleteSchedule(@Param('id') id: string, @Req() req: any) {
67+
return this.reportsService.deleteSchedule(id, req.user?.id);
68+
}
69+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
3+
import { Asset } from '../assets/asset.entity';
4+
import { ReportSchedule } from './report-schedule.entity';
5+
import { ReportsService } from './reports.service';
6+
import { ReportsController } from './reports.controller';
7+
8+
@Module({
9+
imports: [TypeOrmModule.forFeature([Asset, ReportSchedule])],
10+
controllers: [ReportsController],
11+
providers: [ReportsService],
12+
exports: [ReportsService],
13+
})
14+
export class ReportsModule {}

0 commit comments

Comments
 (0)