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
82 changes: 82 additions & 0 deletions backend/src/opsce/assets/assets.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,68 @@ export class AssetsController {
const perPageNumber = parseInt(perPage, 10) || 20;
return this.assetsService.findAll(filter, pageNumber, perPageNumber);
import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
Param,
Patch,
Post,
Query,
Req,
UnauthorizedException,
UsePipes,
ValidationPipe,
DefaultValuePipe,
ParseIntPipe,
} from '@nestjs/common';
import { Request } from 'express';
import { AssetsService } from './assets.service';
import { CreateAssetDto } from './dto/create-asset.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
import { TransferAssetDto } from './dto/transfer-asset.dto';
import { UserRole } from '../users/entities/user.entity';

@Controller('assets')
export class AssetsController {
constructor(private readonly assetsService: AssetsService) {}

private getUser(req: Request) {
const user = req.user as { id: string; role?: string } | undefined;
if (!user || !user.id) {
throw new UnauthorizedException('Authentication required');
}
return user;
}

private assertRole(user: { role?: string }, allowedRoles: string[]) {
if (!allowedRoles.includes(user.role)) {
throw new ForbiddenException('Insufficient permissions');
}
}

@Post()
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
async create(@Req() req: Request, @Body() dto: CreateAssetDto) {
const user = this.getUser(req);
this.assertRole(user, [UserRole.ADMIN, UserRole.MANAGER]);
return this.assetsService.create(dto, user.id);
}

@Get()
async findAll(
@Req() req: Request,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page = 1,
@Query('limit', new DefaultValuePipe(25), ParseIntPipe) limit = 25,
) {
this.getUser(req);
return this.assetsService.findAll(page, limit);
}

@Get(':id')
async findOne(@Req() req: Request, @Param('id') id: string) {
this.getUser(req);
Controller,
Get,
Post,
Expand Down Expand Up @@ -96,6 +158,26 @@ export class AssetsController {
}

@Patch(':id')
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
async update(@Req() req: Request, @Param('id') id: string, @Body() dto: UpdateAssetDto) {
const user = this.getUser(req);
this.assertRole(user, [UserRole.ADMIN, UserRole.MANAGER]);
return this.assetsService.update(id, dto, user.id);
}

@Delete(':id')
async remove(@Req() req: Request, @Param('id') id: string) {
const user = this.getUser(req);
this.assertRole(user, [UserRole.ADMIN]);
return this.assetsService.softRemove(id, user.id);
}

@Post(':id/transfer')
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
async transfer(@Req() req: Request, @Param('id') id: string, @Body() dto: TransferAssetDto) {
const user = this.getUser(req);
this.assertRole(user, [UserRole.ADMIN, UserRole.MANAGER]);
return this.assetsService.transfer(id, dto, user.id);
@Roles(UserRole.ADMIN)
@ApiOperation({ summary: 'Update an asset (ADMIN only)' })
@ApiParam({ name: 'id', description: 'Asset ID' })
Expand Down
1 change: 1 addition & 0 deletions backend/src/opsce/assets/assets.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { AssetsService } from './assets.service';
imports: [TypeOrmModule.forFeature([Asset])],
controllers: [AssetsController],
providers: [AssetsService],
exports: [TypeOrmModule, AssetsService],
exports: [AssetsService],
})
export class AssetsModule {}
183 changes: 183 additions & 0 deletions backend/src/opsce/assets/assets.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,189 @@ import { FilterAssetsDto } from './dto/filter-assets.dto';
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuditService } from '../../audit/audit.service';
import { Asset } from './entities/asset.entity';
import { CreateAssetDto } from './dto/create-asset.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
import { TransferAssetDto } from './dto/transfer-asset.dto';

@Injectable()
export class AssetsService {
private readonly relations = ['assignedToUser', 'department', 'location'];

constructor(
@InjectRepository(Asset)
private readonly assetRepo: Repository<Asset>,
private readonly auditService: AuditService,
) {}

async create(dto: CreateAssetDto, userId?: string): Promise<Asset> {
const payload = this.mapDtoToEntity(dto) as Partial<Asset>;
if (dto.assignedTo) {
payload.assignedAt = new Date();
}
if (userId) {
payload.createdBy = userId;
}

const asset = this.assetRepo.create(payload);
const saved = await this.assetRepo.save(asset);

await this.auditService.log({
userId,
action: 'ASSET_CREATED',
resourceType: 'asset',
resourceId: saved.id,
newValue: saved,
});

return this.findOne(saved.id);
}

async findAll(page = 1, limit = 25): Promise<{ data: Asset[]; total: number; page: number; limit: number }> {
const [data, total] = await this.assetRepo.findAndCount({
relations: this.relations,
order: { createdAt: 'DESC' },
take: limit,
skip: (page - 1) * limit,
});
return { data, total, page, limit };
}

async findOne(id: string): Promise<Asset> {
const asset = await this.assetRepo.findOne({
where: { id },
relations: this.relations,
});

if (!asset) {
throw new NotFoundException('Asset not found');
}

return asset;
}

async update(id: string, dto: UpdateAssetDto, userId?: string): Promise<Asset> {
const asset = await this.assetRepo.findOne({ where: { id } });

if (!asset) {
throw new NotFoundException('Asset not found');
}

const oldValue = {
assignedTo: asset.assignedToUserId,
departmentId: asset.departmentId,
locationId: asset.locationId,
name: asset.name,
category: asset.category,
status: asset.status,
condition: asset.condition,
};

const payload = this.mapDtoToEntity(dto) as Partial<Asset>;
Object.entries(payload).forEach(([key, value]) => {
if (value !== undefined) {
(asset as any)[key] = value;
}
});

if (dto.assignedTo && dto.assignedTo !== asset.assignedToUserId) {
asset.assignedAt = new Date();
}
if (userId) {
asset.updatedBy = userId;
}

const updated = await this.assetRepo.save(asset);

await this.auditService.log({
userId,
action: 'ASSET_UPDATED',
resourceType: 'asset',
resourceId: updated.id,
oldValue,
newValue: updated,
});

return this.findOne(updated.id);
}

async transfer(id: string, dto: TransferAssetDto, userId?: string): Promise<Asset> {
const asset = await this.assetRepo.findOne({ where: { id } });

if (!asset) {
throw new NotFoundException('Asset not found');
}

const oldValue = {
assignedTo: asset.assignedToUserId,
departmentId: asset.departmentId,
locationId: asset.locationId,
};

if (dto.assignedTo !== undefined) {
asset.assignedToUserId = dto.assignedTo;
asset.assignedAt = new Date();
}
if (dto.departmentId !== undefined) {
asset.departmentId = dto.departmentId;
}
if (dto.locationId !== undefined) {
asset.locationId = dto.locationId;
}
if (userId) {
asset.updatedBy = userId;
}

const updated = await this.assetRepo.save(asset);

await this.auditService.log({
userId,
action: 'ASSET_TRANSFER',
resourceType: 'asset',
resourceId: updated.id,
oldValue,
newValue: {
assignedTo: updated.assignedToUserId,
departmentId: updated.departmentId,
locationId: updated.locationId,
},
});

return this.findOne(updated.id);
}

async softRemove(id: string, userId?: string): Promise<Asset> {
const asset = await this.assetRepo.findOne({ where: { id } });

if (!asset) {
throw new NotFoundException('Asset not found');
}

const oldValue = { ...asset };
asset.deletedBy = userId;

const deletedAsset = await this.assetRepo.softRemove(asset);

await this.auditService.log({
userId,
action: 'ASSET_DELETED',
resourceType: 'asset',
resourceId: deletedAsset.id,
oldValue,
newValue: { deletedAt: deletedAsset.deletedAt, deletedBy: deletedAsset.deletedBy },
});

return deletedAsset;
}

private mapDtoToEntity(dto: CreateAssetDto | UpdateAssetDto | TransferAssetDto): Record<string, unknown> {
const payload = { ...dto } as Record<string, unknown>;
if ('assignedTo' in payload) {
payload.assignedToUserId = payload.assignedTo;
delete payload.assignedTo;
}
return payload;
import { Asset } from './entities/asset.entity';
import { CreateAssetDto } from './dto/create-asset.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
Expand Down
Loading
Loading