diff --git a/backend/src/activity-log/activity-log.controller.ts b/backend/src/activity-log/activity-log.controller.ts new file mode 100644 index 00000000..35ecd1e1 --- /dev/null +++ b/backend/src/activity-log/activity-log.controller.ts @@ -0,0 +1,37 @@ +import { Controller, Get, Post, Delete, Param, Body, Query, Req, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { ActivityLogService } from './activity-log.service'; +import { CreateActivityLogDto } from './dtos/create-activity-log.dto'; +import { ActivityLogQueryDto } from './dtos/activity-log-query.dto'; + +@Controller('activity-logs') +@UseGuards(AuthGuard('jwt')) +export class ActivityLogController { + constructor(private readonly activityLogService: ActivityLogService) {} + + @Post() + async create(@Body() dto: CreateActivityLogDto, @Req() req: any) { + return this.activityLogService.create({ + ...dto, + userId: req.user?.id, + ipAddress: req.ip, + userAgent: req.headers['user-agent'], + }); + } + + @Get() + async findAll(@Query() query: ActivityLogQueryDto) { + return this.activityLogService.findAll(query); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + return this.activityLogService.findById(id); + } + + @Delete(':id') + async remove(@Param('id') id: string) { + await this.activityLogService.remove(id); + return { message: 'Activity log deleted successfully' }; + } +} diff --git a/backend/src/activity-log/activity-log.module.ts b/backend/src/activity-log/activity-log.module.ts new file mode 100644 index 00000000..930d1643 --- /dev/null +++ b/backend/src/activity-log/activity-log.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ActivityLog } from './entities/activity-log.entity'; +import { ActivityLogService } from './activity-log.service'; +import { ActivityLogController } from './activity-log.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([ActivityLog])], + controllers: [ActivityLogController], + providers: [ActivityLogService], + exports: [ActivityLogService], +}) +export class ActivityLogModule {} diff --git a/backend/src/activity-log/activity-log.service.ts b/backend/src/activity-log/activity-log.service.ts new file mode 100644 index 00000000..39f673bf --- /dev/null +++ b/backend/src/activity-log/activity-log.service.ts @@ -0,0 +1,52 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ActivityLog } from './entities/activity-log.entity'; +import { CreateActivityLogDto } from './dtos/create-activity-log.dto'; +import { ActivityLogQueryDto } from './dtos/activity-log-query.dto'; + +@Injectable() +export class ActivityLogService { + constructor( + @InjectRepository(ActivityLog) + private readonly activityLogRepository: Repository, + ) {} + + async create(dto: CreateActivityLogDto): Promise { + const log = this.activityLogRepository.create(dto); + return this.activityLogRepository.save(log); + } + + async findAll(query: ActivityLogQueryDto): Promise<{ data: ActivityLog[]; total: number }> { + const { page = 1, limit = 20, userId, action, entityType, entityId, startDate, endDate } = query; + const qb = this.activityLogRepository.createQueryBuilder('log') + .leftJoinAndSelect('log.user', 'user') + .skip((page - 1) * limit) + .take(limit) + .orderBy('log.createdAt', 'DESC'); + + if (userId) qb.andWhere('log.userId = :userId', { userId }); + if (action) qb.andWhere('log.action = :action', { action }); + if (entityType) qb.andWhere('log.entityType = :entityType', { entityType }); + if (entityId) qb.andWhere('log.entityId = :entityId', { entityId }); + if (startDate) qb.andWhere('log.createdAt >= :startDate', { startDate }); + if (endDate) qb.andWhere('log.createdAt <= :endDate', { endDate }); + + const [data, total] = await qb.getManyAndCount(); + return { data, total }; + } + + async findById(id: string): Promise { + const log = await this.activityLogRepository.findOne({ + where: { id }, + relations: ['user'], + }); + if (!log) throw new NotFoundException('Activity log not found'); + return log; + } + + async remove(id: string): Promise { + const log = await this.findById(id); + await this.activityLogRepository.softDelete(log.id); + } +} diff --git a/backend/src/activity-log/dtos/activity-log-query.dto.ts b/backend/src/activity-log/dtos/activity-log-query.dto.ts new file mode 100644 index 00000000..e3fdf470 --- /dev/null +++ b/backend/src/activity-log/dtos/activity-log-query.dto.ts @@ -0,0 +1,41 @@ +import { IsOptional, IsString, IsInt, Min, Max } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class ActivityLogQueryDto { + @IsOptional() + @IsString() + userId?: string; + + @IsOptional() + @IsString() + action?: string; + + @IsOptional() + @IsString() + entityType?: string; + + @IsOptional() + @IsString() + entityId?: string; + + @IsOptional() + @IsString() + startDate?: string; + + @IsOptional() + @IsString() + endDate?: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number = 20; +} diff --git a/backend/src/activity-log/dtos/create-activity-log.dto.ts b/backend/src/activity-log/dtos/create-activity-log.dto.ts new file mode 100644 index 00000000..8c8a9a6f --- /dev/null +++ b/backend/src/activity-log/dtos/create-activity-log.dto.ts @@ -0,0 +1,28 @@ +import { IsString, IsOptional } from 'class-validator'; + +export class CreateActivityLogDto { + @IsOptional() + @IsString() + userId?: string; + + @IsString() + action: string; + + @IsString() + entityType: string; + + @IsOptional() + @IsString() + entityId?: string; + + @IsOptional() + metadata?: Record; + + @IsOptional() + @IsString() + ipAddress?: string; + + @IsOptional() + @IsString() + userAgent?: string; +} diff --git a/backend/src/activity-log/entities/activity-log.entity.ts b/backend/src/activity-log/entities/activity-log.entity.ts new file mode 100644 index 00000000..828d6a7f --- /dev/null +++ b/backend/src/activity-log/entities/activity-log.entity.ts @@ -0,0 +1,39 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { User } from '../../users/entities/user.entity'; + +@Entity('activity_logs') +export class ActivityLog { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: true }) + userId: string; + + @ManyToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'userId' }) + user: User; + + @Column() + action: string; + + @Column() + entityType: string; + + @Column({ nullable: true }) + entityId: string; + + @Column({ type: 'jsonb', nullable: true }) + metadata: Record; + + @Column({ nullable: true }) + ipAddress: string; + + @Column({ nullable: true }) + userAgent: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index e543f3a4..d4a1408a 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -18,6 +18,7 @@ import { AssetsModule } from './assets/assets.module'; import { QueueModule } from './queue/queue.module'; import { StorageModule } from './storage/storage.module'; import { CacheService } from './cache/cache.service'; +import { ActivityLogModule } from './activity-log/activity-log.module'; import { LocationsModule } from './locations/locations.module'; import { ContractsModule } from './contracts/contracts.module'; import { LicensesModule } from './licenses/licenses.module'; @@ -81,6 +82,8 @@ import { TasksModule } from './tasks/tasks.module'; TasksModule, ], LocationsModule, + ], + ActivityLogModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/assets/asset.entity.ts b/backend/src/assets/asset.entity.ts index 72110289..83779b55 100644 --- a/backend/src/assets/asset.entity.ts +++ b/backend/src/assets/asset.entity.ts @@ -91,6 +91,12 @@ export class Asset { @Column({ nullable: true }) qrCode: string; + @Column({ type: 'date', nullable: true }) + endOfLife: string; + + @Column({ default: false }) + endOfLifeNotificationSent: boolean; + @Column({ nullable: true, type: 'text' }) notes: string; diff --git a/backend/src/assets/dtos/create-asset.dto.ts b/backend/src/assets/dtos/create-asset.dto.ts index 5f922582..d376e9f5 100644 --- a/backend/src/assets/dtos/create-asset.dto.ts +++ b/backend/src/assets/dtos/create-asset.dto.ts @@ -95,6 +95,13 @@ export class CreateAssetDto { @IsString() model?: string; + @IsOptional() + @IsString() + endOfLife?: string; + + @IsOptional() + endOfLifeNotificationSent?: boolean; + @IsOptional() @IsString() notes?: string; diff --git a/backend/src/assets/dtos/update-asset.dto.ts b/backend/src/assets/dtos/update-asset.dto.ts index 0d9d6f91..cac41fa2 100644 --- a/backend/src/assets/dtos/update-asset.dto.ts +++ b/backend/src/assets/dtos/update-asset.dto.ts @@ -96,6 +96,13 @@ export class UpdateAssetDto { @IsString() model?: string; + @IsOptional() + @IsString() + endOfLife?: string; + + @IsOptional() + endOfLifeNotificationSent?: boolean; + @IsOptional() @IsString() notes?: string; diff --git a/backend/src/assets/entities/asset.entity.ts b/backend/src/assets/entities/asset.entity.ts index ea08dab1..4688f575 100644 --- a/backend/src/assets/entities/asset.entity.ts +++ b/backend/src/assets/entities/asset.entity.ts @@ -91,6 +91,12 @@ export class Asset { @Column({ nullable: true }) qrCode: string; + @Column({ type: 'date', nullable: true }) + endOfLife: string; + + @Column({ default: false }) + endOfLifeNotificationSent: boolean; + @Column({ nullable: true, type: 'text' }) notes: string; diff --git a/backend/src/common/logger/logger.service.ts b/backend/src/common/logger/logger.service.ts new file mode 100644 index 00000000..0f3a3ba9 --- /dev/null +++ b/backend/src/common/logger/logger.service.ts @@ -0,0 +1,45 @@ +import { Injectable, Logger, LoggerService as NestLoggerService, LogLevel } from '@nestjs/common'; + +@Injectable() +export class LoggerService implements NestLoggerService { + private readonly logger: Logger; + private context: string; + + constructor(context = 'Application') { + this.logger = new Logger(context); + this.context = context; + } + + setContext(context: string): void { + this.context = context; + (this.logger as any).context = context; + } + + log(message: any, context?: string): void { + this.logger.log(message, context || this.context); + } + + error(message: any, trace?: string, context?: string): void { + this.logger.error(message, trace, context || this.context); + } + + warn(message: any, context?: string): void { + this.logger.warn(message, context || this.context); + } + + debug(message: any, context?: string): void { + this.logger.debug(message, context || this.context); + } + + verbose(message: any, context?: string): void { + this.logger.verbose(message, context || this.context); + } + + fatal(message: any, context?: string): void { + this.logger.fatal(message, context || this.context); + } + + setLogLevels(levels: LogLevel[]): void { + this.logger.localInstance.setLogLevels(levels); + } +} diff --git a/backend/src/users/departments.controller.ts b/backend/src/users/departments.controller.ts new file mode 100644 index 00000000..7446b747 --- /dev/null +++ b/backend/src/users/departments.controller.ts @@ -0,0 +1,44 @@ +import { Controller, Get, Post, Put, Delete, Param, Body, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Department } from './entities/department.entity'; +import { CreateDepartmentDto } from './dtos/create-department.dto'; +import { UpdateDepartmentDto } from './dtos/update-department.dto'; + +@Controller('departments') +@UseGuards(AuthGuard('jwt')) +export class DepartmentsController { + constructor( + @InjectRepository(Department) + private readonly departmentRepository: Repository, + ) {} + + @Get() + async findAll() { + return this.departmentRepository.find(); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + return this.departmentRepository.findOne({ where: { id } }); + } + + @Post() + async create(@Body() dto: CreateDepartmentDto) { + const department = this.departmentRepository.create(dto); + return this.departmentRepository.save(department); + } + + @Put(':id') + async update(@Param('id') id: string, @Body() dto: UpdateDepartmentDto) { + await this.departmentRepository.update(id, dto); + return this.departmentRepository.findOne({ where: { id } }); + } + + @Delete(':id') + async remove(@Param('id') id: string) { + await this.departmentRepository.delete(id); + return { message: 'Department deleted' }; + } +} diff --git a/backend/src/users/dtos/create-department.dto.ts b/backend/src/users/dtos/create-department.dto.ts new file mode 100644 index 00000000..d0c36545 --- /dev/null +++ b/backend/src/users/dtos/create-department.dto.ts @@ -0,0 +1,14 @@ +import { IsString, IsOptional } from 'class-validator'; + +export class CreateDepartmentDto { + @IsString() + name: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsString() + parentId?: string; +} diff --git a/backend/src/users/dtos/update-department.dto.ts b/backend/src/users/dtos/update-department.dto.ts new file mode 100644 index 00000000..6c0cac64 --- /dev/null +++ b/backend/src/users/dtos/update-department.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsOptional } from 'class-validator'; + +export class UpdateDepartmentDto { + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsString() + parentId?: string; +} diff --git a/backend/src/users/users.module.ts b/backend/src/users/users.module.ts index 01269552..efba29bf 100644 --- a/backend/src/users/users.module.ts +++ b/backend/src/users/users.module.ts @@ -6,6 +6,7 @@ import { Department } from './entities/department.entity'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; import { RolesController } from './roles.controller'; +import { DepartmentsController } from './departments.controller'; import { StorageModule } from '../storage/storage.module'; @Module({ @@ -13,7 +14,7 @@ import { StorageModule } from '../storage/storage.module'; TypeOrmModule.forFeature([User, Role, Department]), StorageModule, ], - controllers: [UsersController, RolesController], + controllers: [UsersController, RolesController, DepartmentsController], providers: [UsersService], exports: [UsersService], })