Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions backend/src/activity-log/activity-log.controller.ts
Original file line number Diff line number Diff line change
@@ -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' };
}
}
13 changes: 13 additions & 0 deletions backend/src/activity-log/activity-log.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
52 changes: 52 additions & 0 deletions backend/src/activity-log/activity-log.service.ts
Original file line number Diff line number Diff line change
@@ -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<ActivityLog>,
) {}

async create(dto: CreateActivityLogDto): Promise<ActivityLog> {
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<ActivityLog> {
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<void> {
const log = await this.findById(id);
await this.activityLogRepository.softDelete(log.id);
}
}
41 changes: 41 additions & 0 deletions backend/src/activity-log/dtos/activity-log-query.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
28 changes: 28 additions & 0 deletions backend/src/activity-log/dtos/create-activity-log.dto.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;

@IsOptional()
@IsString()
ipAddress?: string;

@IsOptional()
@IsString()
userAgent?: string;
}
39 changes: 39 additions & 0 deletions backend/src/activity-log/entities/activity-log.entity.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;

@Column({ nullable: true })
ipAddress: string;

@Column({ nullable: true })
userAgent: string;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
3 changes: 3 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -81,6 +82,8 @@ import { TasksModule } from './tasks/tasks.module';
TasksModule,
],
LocationsModule,
],
ActivityLogModule,
],
controllers: [AppController],
providers: [
Expand Down
6 changes: 6 additions & 0 deletions backend/src/assets/asset.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
7 changes: 7 additions & 0 deletions backend/src/assets/dtos/create-asset.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ export class CreateAssetDto {
@IsString()
model?: string;

@IsOptional()
@IsString()
endOfLife?: string;

@IsOptional()
endOfLifeNotificationSent?: boolean;

@IsOptional()
@IsString()
notes?: string;
Expand Down
7 changes: 7 additions & 0 deletions backend/src/assets/dtos/update-asset.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ export class UpdateAssetDto {
@IsString()
model?: string;

@IsOptional()
@IsString()
endOfLife?: string;

@IsOptional()
endOfLifeNotificationSent?: boolean;

@IsOptional()
@IsString()
notes?: string;
Expand Down
6 changes: 6 additions & 0 deletions backend/src/assets/entities/asset.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
45 changes: 45 additions & 0 deletions backend/src/common/logger/logger.service.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
44 changes: 44 additions & 0 deletions backend/src/users/departments.controller.ts
Original file line number Diff line number Diff line change
@@ -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<Department>,
) {}

@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' };
}
}
14 changes: 14 additions & 0 deletions backend/src/users/dtos/create-department.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IsString, IsOptional } from 'class-validator';

export class CreateDepartmentDto {
@IsString()
name: string;

@IsOptional()
@IsString()
description?: string;

@IsOptional()
@IsString()
parentId?: string;
}
Loading
Loading