diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 27fcdb8..cab74ce 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -37,7 +37,7 @@ import { SearchModule } from './search/search.module'; UsersModule, SearchModule, ], - controllers: [AppController], - providers: [AppService], + controllers: [AppController, NotificationsController], + providers: [AppService, NotificationsService], }) export class AppModule {} diff --git a/backend/src/notifications/dto/create-notification.dto.ts b/backend/src/notifications/dto/create-notification.dto.ts new file mode 100644 index 0000000..1e6fcc3 --- /dev/null +++ b/backend/src/notifications/dto/create-notification.dto.ts @@ -0,0 +1,19 @@ +import { IsString, IsNotEmpty, IsOptional, IsObject } from 'class-validator'; + +export class CreateNotificationDto { + @IsString() + @IsNotEmpty() + userId: string; + + @IsString() + @IsNotEmpty() + message: string; + + @IsString() + @IsOptional() + type?: string; + + @IsObject() + @IsOptional() + metadata?: Record; +} diff --git a/backend/src/notifications/dto/query-notification.dto.ts b/backend/src/notifications/dto/query-notification.dto.ts new file mode 100644 index 0000000..fdcce4b --- /dev/null +++ b/backend/src/notifications/dto/query-notification.dto.ts @@ -0,0 +1,17 @@ +import { IsOptional, IsString, IsBoolean } from 'class-validator'; +import { Transform } from 'class-transformer'; + +export class QueryNotificationDto { + @IsOptional() + @IsString() + userId?: string; + + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === 'true' || value === true) + isRead?: boolean; + + @IsOptional() + @IsString() + type?: string; +} diff --git a/backend/src/notifications/dto/update-notification.dto.ts b/backend/src/notifications/dto/update-notification.dto.ts new file mode 100644 index 0000000..a79056c --- /dev/null +++ b/backend/src/notifications/dto/update-notification.dto.ts @@ -0,0 +1,7 @@ +import { IsBoolean, IsOptional } from 'class-validator'; + +export class UpdateNotificationDto { + @IsBoolean() + @IsOptional() + isRead?: boolean; +} diff --git a/backend/src/notifications/entities/notification.entity.ts b/backend/src/notifications/entities/notification.entity.ts new file mode 100644 index 0000000..f119547 --- /dev/null +++ b/backend/src/notifications/entities/notification.entity.ts @@ -0,0 +1,34 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('notifications') +export class Notification { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + userId: string; + + @Column('text') + message: string; + + @Column({ default: false }) + isRead: boolean; + + @Column({ nullable: true }) + type: string; // 'asset_transfer', 'low_stock', 'maintenance_due' + + @Column('json', { nullable: true }) + metadata: Record; // Additional data for the notification + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/notifications/notifications.controller.ts b/backend/src/notifications/notifications.controller.ts new file mode 100644 index 0000000..594251a --- /dev/null +++ b/backend/src/notifications/notifications.controller.ts @@ -0,0 +1,115 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import type { NotificationsService } from './notifications.service'; +import type { CreateNotificationDto } from './dto/create-notification.dto'; +import type { UpdateNotificationDto } from './dto/update-notification.dto'; +import type { QueryNotificationDto } from './dto/query-notification.dto'; + +@Controller('notifications') +export class NotificationsController { + constructor(private readonly notificationsService: NotificationsService) {} + + @Post() + @HttpCode(HttpStatus.CREATED) + create(createNotificationDto: CreateNotificationDto) { + return this.notificationsService.create(createNotificationDto); + } + + @Get() + findAll(query: QueryNotificationDto) { + return this.notificationsService.findAll(query); + } + + @Get('user/:userId') + findByUserId(userId: string) { + return this.notificationsService.findByUserId(userId); + } + + @Get('user/:userId/unread-count') + getUnreadCount(userId: string) { + return this.notificationsService.getUnreadCount(userId); + } + + @Get(':id') + findOne(id: string) { + return this.notificationsService.findOne(id); + } + + @Patch(':id') + update(id: string, updateNotificationDto: UpdateNotificationDto) { + return this.notificationsService.update(id, updateNotificationDto); + } + + @Patch(':id/mark-read') + @HttpCode(HttpStatus.OK) + markAsRead(id: string) { + return this.notificationsService.markAsRead(id); + } + + @Patch('user/:userId/mark-all-read') + @HttpCode(HttpStatus.OK) + async markAllAsRead(userId: string) { + await this.notificationsService.markAllAsRead(userId); + return { message: 'All notifications marked as read' }; + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + remove(id: string) { + return this.notificationsService.remove(id); + } + + // Specific notification creation endpoints + @Post('asset-transfer') + @HttpCode(HttpStatus.CREATED) + createAssetTransfer(body: { + userId: string; + assetName: string; + from: string; + to: string; + }) { + return this.notificationsService.createAssetTransferNotification( + body.userId, + body.assetName, + body.from, + body.to, + ); + } + + @Post('low-stock') + @HttpCode(HttpStatus.CREATED) + createLowStock(body: { + userId: string; + itemName: string; + currentStock: number; + threshold: number; + }) { + return this.notificationsService.createLowStockNotification( + body.userId, + body.itemName, + body.currentStock, + body.threshold, + ); + } + + @Post('maintenance-due') + @HttpCode(HttpStatus.CREATED) + createMaintenanceDue(body: { + userId: string; + assetName: string; + dueDate: string; + }) { + return this.notificationsService.createMaintenanceDueNotification( + body.userId, + body.assetName, + new Date(body.dueDate), + ); + } +} diff --git a/backend/src/notifications/notifications.module.ts b/backend/src/notifications/notifications.module.ts new file mode 100644 index 0000000..7a91cf3 --- /dev/null +++ b/backend/src/notifications/notifications.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotificationsService } from './notifications.service'; +import { NotificationsController } from './notifications.controller'; +import { Notification } from './entities/notification.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Notification])], + controllers: [NotificationsController], + providers: [NotificationsService], + exports: [NotificationsService], // Export for use in other modules +}) +export class NotificationsModule {} diff --git a/backend/src/notifications/notifications.service.ts b/backend/src/notifications/notifications.service.ts new file mode 100644 index 0000000..20ea97f --- /dev/null +++ b/backend/src/notifications/notifications.service.ts @@ -0,0 +1,139 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import type { Repository } from 'typeorm'; +import type { Notification } from './entities/notification.entity'; +import type { CreateNotificationDto } from './dto/create-notification.dto'; +import type { UpdateNotificationDto } from './dto/update-notification.dto'; +import type { QueryNotificationDto } from './dto/query-notification.dto'; + +@Injectable() +export class NotificationsService { + private notificationsRepository: Repository; + + constructor(notificationsRepository: Repository) { + this.notificationsRepository = notificationsRepository; + } + + async create( + createNotificationDto: CreateNotificationDto, + ): Promise { + const notification = this.notificationsRepository.create( + createNotificationDto, + ); + return await this.notificationsRepository.save(notification); + } + + async findAll(query: QueryNotificationDto): Promise { + const where: any = {}; + + if (query.userId) { + where.userId = query.userId; + } + + if (query.isRead !== undefined) { + where.isRead = query.isRead; + } + + if (query.type) { + where.type = query.type; + } + + return await this.notificationsRepository.find({ + where, + order: { createdAt: 'DESC' }, + }); + } + + async findOne(id: string): Promise { + const notification = await this.notificationsRepository.findOne({ + where: { id }, + }); + + if (!notification) { + throw new NotFoundException(`Notification with ID ${id} not found`); + } + + return notification; + } + + async findByUserId(userId: string): Promise { + return await this.notificationsRepository.find({ + where: { userId }, + order: { createdAt: 'DESC' }, + }); + } + + async getUnreadCount(userId: string): Promise { + return await this.notificationsRepository.count({ + where: { userId, isRead: false }, + }); + } + + async markAsRead(id: string): Promise { + const notification = await this.findOne(id); + notification.isRead = true; + return await this.notificationsRepository.save(notification); + } + + async markAllAsRead(userId: string): Promise { + await this.notificationsRepository.update( + { userId, isRead: false }, + { isRead: true }, + ); + } + + async update( + id: string, + updateNotificationDto: UpdateNotificationDto, + ): Promise { + const notification = await this.findOne(id); + Object.assign(notification, updateNotificationDto); + return await this.notificationsRepository.save(notification); + } + + async remove(id: string): Promise { + const notification = await this.findOne(id); + await this.notificationsRepository.remove(notification); + } + + // Helper methods for creating specific notification types + async createAssetTransferNotification( + userId: string, + assetName: string, + from: string, + to: string, + ): Promise { + return this.create({ + userId, + message: `Asset "${assetName}" has been transferred from ${from} to ${to}`, + type: 'asset_transfer', + metadata: { assetName, from, to }, + }); + } + + async createLowStockNotification( + userId: string, + itemName: string, + currentStock: number, + threshold: number, + ): Promise { + return this.create({ + userId, + message: `Low stock alert: "${itemName}" has only ${currentStock} units remaining (threshold: ${threshold})`, + type: 'low_stock', + metadata: { itemName, currentStock, threshold }, + }); + } + + async createMaintenanceDueNotification( + userId: string, + assetName: string, + dueDate: Date, + ): Promise { + return this.create({ + userId, + message: `Maintenance due for "${assetName}" on ${dueDate.toLocaleDateString()}`, + type: 'maintenance_due', + metadata: { assetName, dueDate }, + }); + } +}