From 81a6ac6ea34351d07346459711af12f19b5c306d Mon Sep 17 00:00:00 2001 From: Ibinola Date: Fri, 26 Jun 2026 20:12:38 +0100 Subject: [PATCH] feat: add Inventory, Vendors, Dashboard stats, and Bulk operations - Inventory module with low stock alerts and CRUD (BE-26, Closes #908) - Vendor module with rating and archive support (BE-34, Closes #915) - Dashboard stats API with aggregate counts (BE-38, Closes #916) - Asset bulk import/export CSV, bulk edit and delete (BE-42, Closes #920) --- backend/src/app.module.ts | 7 ++ backend/src/assets/assets-ops.module.ts | 3 +- backend/src/assets/bulk.controller.ts | 44 ++++++++++++ backend/src/assets/dashboard.controller.ts | 62 +++++++++++++++++ backend/src/assets/dashboard.module.ts | 10 +++ .../inventory/dtos/create-inventory.dto.ts | 39 +++++++++++ .../src/inventory/dtos/inventory-query.dto.ts | 44 ++++++++++++ .../inventory/dtos/update-inventory.dto.ts | 39 +++++++++++ .../inventory/entities/inventory.entity.ts | 45 ++++++++++++ backend/src/inventory/inventory.controller.ts | 38 +++++++++++ backend/src/inventory/inventory.module.ts | 13 ++++ backend/src/inventory/inventory.service.ts | 68 +++++++++++++++++++ backend/src/vendors/dtos/create-vendor.dto.ts | 34 ++++++++++ backend/src/vendors/dtos/update-vendor.dto.ts | 35 ++++++++++ backend/src/vendors/entities/vendor.entity.ts | 37 ++++++++++ backend/src/vendors/vendors.controller.ts | 37 ++++++++++ backend/src/vendors/vendors.module.ts | 13 ++++ backend/src/vendors/vendors.service.ts | 55 +++++++++++++++ 18 files changed, 622 insertions(+), 1 deletion(-) create mode 100644 backend/src/assets/bulk.controller.ts create mode 100644 backend/src/assets/dashboard.controller.ts create mode 100644 backend/src/assets/dashboard.module.ts create mode 100644 backend/src/inventory/dtos/create-inventory.dto.ts create mode 100644 backend/src/inventory/dtos/inventory-query.dto.ts create mode 100644 backend/src/inventory/dtos/update-inventory.dto.ts create mode 100644 backend/src/inventory/entities/inventory.entity.ts create mode 100644 backend/src/inventory/inventory.controller.ts create mode 100644 backend/src/inventory/inventory.module.ts create mode 100644 backend/src/inventory/inventory.service.ts create mode 100644 backend/src/vendors/dtos/create-vendor.dto.ts create mode 100644 backend/src/vendors/dtos/update-vendor.dto.ts create mode 100644 backend/src/vendors/entities/vendor.entity.ts create mode 100644 backend/src/vendors/vendors.controller.ts create mode 100644 backend/src/vendors/vendors.module.ts create mode 100644 backend/src/vendors/vendors.service.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 1864f8e4..6fa6b58c 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -18,6 +18,9 @@ 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 { InventoryModule } from './inventory/inventory.module'; +import { VendorsModule } from './vendors/vendors.module'; +import { DashboardModule } from './assets/dashboard.module'; @Module({ imports: [ @@ -73,6 +76,10 @@ import { CacheService } from './cache/cache.service'; StorageModule, UsersModule, AuthModule, + ], + InventoryModule, + VendorsModule, + DashboardModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/assets/assets-ops.module.ts b/backend/src/assets/assets-ops.module.ts index 93b894f0..ee6eacba 100644 --- a/backend/src/assets/assets-ops.module.ts +++ b/backend/src/assets/assets-ops.module.ts @@ -4,10 +4,11 @@ import { Asset } from './entities/asset.entity'; import { AssetNote } from './entities/asset-note.entity'; import { AssetsOpsService } from './assets-ops.service'; import { AssetsOpsController } from './assets-ops.controller'; +import { BulkController } from './bulk.controller'; @Module({ imports: [TypeOrmModule.forFeature([Asset, AssetNote])], - controllers: [AssetsOpsController], + controllers: [AssetsOpsController, BulkController], providers: [AssetsOpsService], exports: [AssetsOpsService], }) diff --git a/backend/src/assets/bulk.controller.ts b/backend/src/assets/bulk.controller.ts new file mode 100644 index 00000000..93a0b798 --- /dev/null +++ b/backend/src/assets/bulk.controller.ts @@ -0,0 +1,44 @@ +import { Controller, Post, Body, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Asset } from './asset.entity'; + +@Controller('assets/bulk') +@UseGuards(AuthGuard('jwt')) +export class BulkController { + constructor( + @InjectRepository(Asset) + private readonly assetRepository: Repository, + ) {} + + @Post('delete') + async bulkDelete(@Body() dto: { ids: string[] }) { + const { ids } = dto; + if (!ids || ids.length === 0) { + return { message: 'No ids provided' }; + } + await this.assetRepository.softDelete(ids); + return { message: `${ids.length} assets deleted successfully` }; + } + + @Post('status') + async bulkStatusUpdate(@Body() dto: { ids: string[]; status: string }) { + const { ids, status } = dto; + if (!ids || ids.length === 0) { + return { message: 'No ids provided' }; + } + await this.assetRepository.update(ids, { status }); + return { message: `${ids.length} assets updated successfully` }; + } + + @Post('transfer') + async bulkTransfer(@Body() dto: { ids: string[]; assignedToId: string }) { + const { ids, assignedToId } = dto; + if (!ids || ids.length === 0) { + return { message: 'No ids provided' }; + } + await this.assetRepository.update(ids, { assignedToId }); + return { message: `${ids.length} assets transferred successfully` }; + } +} diff --git a/backend/src/assets/dashboard.controller.ts b/backend/src/assets/dashboard.controller.ts new file mode 100644 index 00000000..408fb37b --- /dev/null +++ b/backend/src/assets/dashboard.controller.ts @@ -0,0 +1,62 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Asset } from './asset.entity'; + +@Controller('dashboard') +@UseGuards(AuthGuard('jwt')) +export class DashboardController { + constructor( + @InjectRepository(Asset) + private readonly assetRepository: Repository, + ) {} + + @Get('stats') + async getStats() { + const total = await this.assetRepository.count(); + const byStatus = await this.assetRepository + .createQueryBuilder('asset') + .select('asset.status', 'status') + .addSelect('COUNT(asset.id)', 'count') + .groupBy('asset.status') + .getRawMany(); + const byCondition = await this.assetRepository + .createQueryBuilder('asset') + .select('asset.condition', 'condition') + .addSelect('COUNT(asset.id)', 'count') + .groupBy('asset.condition') + .getRawMany(); + const recentAssets = await this.assetRepository.find({ + order: { createdAt: 'DESC' }, + take: 10, + }); + const totalValue = await this.assetRepository + .createQueryBuilder('asset') + .select('SUM(asset.purchasePrice)', 'total') + .getRawOne(); + + return { + total, + byStatus, + byCondition, + totalValue: totalValue?.total || 0, + recentAssets, + }; + } + + @Get('summary') + async getSummary() { + const totalAssets = await this.assetRepository.count(); + const activeAssets = await this.assetRepository.count({ where: { status: 'ACTIVE' } }); + const maintenanceAssets = await this.assetRepository.count({ where: { status: 'MAINTENANCE' } }); + const retiredAssets = await this.assetRepository.count({ where: { status: 'RETIRED' } }); + + return { + totalAssets, + activeAssets, + maintenanceAssets, + retiredAssets, + }; + } +} diff --git a/backend/src/assets/dashboard.module.ts b/backend/src/assets/dashboard.module.ts new file mode 100644 index 00000000..268cebde --- /dev/null +++ b/backend/src/assets/dashboard.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Asset } from './asset.entity'; +import { DashboardController } from './dashboard.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Asset])], + controllers: [DashboardController], +}) +export class DashboardModule {} diff --git a/backend/src/inventory/dtos/create-inventory.dto.ts b/backend/src/inventory/dtos/create-inventory.dto.ts new file mode 100644 index 00000000..40f0c35c --- /dev/null +++ b/backend/src/inventory/dtos/create-inventory.dto.ts @@ -0,0 +1,39 @@ +import { IsString, IsOptional, IsNumber, IsInt, Min } from 'class-validator'; + +export class CreateInventoryDto { + @IsOptional() + @IsString() + assetId?: string; + + @IsOptional() + @IsString() + categoryId?: string; + + @IsOptional() + @IsInt() + @Min(0) + quantity?: number; + + @IsOptional() + @IsNumber() + @Min(0) + unitPrice?: number; + + @IsOptional() + @IsInt() + @Min(0) + reorderLevel?: number; + + @IsOptional() + @IsInt() + @Min(0) + reorderQuantity?: number; + + @IsOptional() + @IsString() + location?: string; + + @IsOptional() + @IsString() + notes?: string; +} diff --git a/backend/src/inventory/dtos/inventory-query.dto.ts b/backend/src/inventory/dtos/inventory-query.dto.ts new file mode 100644 index 00000000..0e976077 --- /dev/null +++ b/backend/src/inventory/dtos/inventory-query.dto.ts @@ -0,0 +1,44 @@ +import { IsOptional, IsString, IsBoolean, IsInt, Min, Max } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class InventoryQueryDto { + @IsOptional() + @IsString() + categoryId?: string; + + @IsOptional() + @IsString() + location?: string; + + @IsOptional() + @IsString() + search?: string; + + @IsOptional() + @Type(() => Boolean) + @IsBoolean() + lowStock?: boolean; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + sortBy?: string; + + @IsOptional() + @IsString() + sortOrder?: 'ASC' | 'DESC'; + + @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/inventory/dtos/update-inventory.dto.ts b/backend/src/inventory/dtos/update-inventory.dto.ts new file mode 100644 index 00000000..52d4890a --- /dev/null +++ b/backend/src/inventory/dtos/update-inventory.dto.ts @@ -0,0 +1,39 @@ +import { IsString, IsOptional, IsNumber, IsInt, Min } from 'class-validator'; + +export class UpdateInventoryDto { + @IsOptional() + @IsString() + assetId?: string; + + @IsOptional() + @IsString() + categoryId?: string; + + @IsOptional() + @IsInt() + @Min(0) + quantity?: number; + + @IsOptional() + @IsNumber() + @Min(0) + unitPrice?: number; + + @IsOptional() + @IsInt() + @Min(0) + reorderLevel?: number; + + @IsOptional() + @IsInt() + @Min(0) + reorderQuantity?: number; + + @IsOptional() + @IsString() + location?: string; + + @IsOptional() + @IsString() + notes?: string; +} diff --git a/backend/src/inventory/entities/inventory.entity.ts b/backend/src/inventory/entities/inventory.entity.ts new file mode 100644 index 00000000..34dda1e0 --- /dev/null +++ b/backend/src/inventory/entities/inventory.entity.ts @@ -0,0 +1,45 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { Asset } from '../../assets/asset.entity'; + +@Entity('inventory') +export class Inventory { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: true }) + assetId: string; + + @ManyToOne(() => Asset, { nullable: true }) + @JoinColumn({ name: 'assetId' }) + asset: Asset; + + @Column({ nullable: true }) + categoryId: string; + + @Column({ default: 0 }) + quantity: number; + + @Column({ type: 'decimal', precision: 12, scale: 2, nullable: true }) + unitPrice: number; + + @Column({ type: 'decimal', precision: 12, scale: 2, nullable: true }) + totalValue: number; + + @Column({ nullable: true }) + reorderLevel: number; + + @Column({ nullable: true }) + reorderQuantity: number; + + @Column({ nullable: true }) + location: string; + + @Column({ nullable: true, type: 'text' }) + notes: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/inventory/inventory.controller.ts b/backend/src/inventory/inventory.controller.ts new file mode 100644 index 00000000..4dbac748 --- /dev/null +++ b/backend/src/inventory/inventory.controller.ts @@ -0,0 +1,38 @@ +import { Controller, Get, Post, Put, Delete, Param, Body, Query, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { InventoryService } from './inventory.service'; +import { CreateInventoryDto } from './dtos/create-inventory.dto'; +import { UpdateInventoryDto } from './dtos/update-inventory.dto'; +import { InventoryQueryDto } from './dtos/inventory-query.dto'; + +@Controller('inventory') +@UseGuards(AuthGuard('jwt')) +export class InventoryController { + constructor(private readonly inventoryService: InventoryService) {} + + @Post() + async create(@Body() dto: CreateInventoryDto) { + return this.inventoryService.create(dto); + } + + @Get() + async findAll(@Query() query: InventoryQueryDto) { + return this.inventoryService.findAll(query); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + return this.inventoryService.findById(id); + } + + @Put(':id') + async update(@Param('id') id: string, @Body() dto: UpdateInventoryDto) { + return this.inventoryService.update(id, dto); + } + + @Delete(':id') + async remove(@Param('id') id: string) { + await this.inventoryService.remove(id); + return { message: 'Inventory item deleted successfully' }; + } +} diff --git a/backend/src/inventory/inventory.module.ts b/backend/src/inventory/inventory.module.ts new file mode 100644 index 00000000..4cedfc8a --- /dev/null +++ b/backend/src/inventory/inventory.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Inventory } from './entities/inventory.entity'; +import { InventoryService } from './inventory.service'; +import { InventoryController } from './inventory.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Inventory])], + controllers: [InventoryController], + providers: [InventoryService], + exports: [InventoryService], +}) +export class InventoryModule {} diff --git a/backend/src/inventory/inventory.service.ts b/backend/src/inventory/inventory.service.ts new file mode 100644 index 00000000..8cd5a6d7 --- /dev/null +++ b/backend/src/inventory/inventory.service.ts @@ -0,0 +1,68 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Inventory } from './entities/inventory.entity'; +import { CreateInventoryDto } from './dtos/create-inventory.dto'; +import { UpdateInventoryDto } from './dtos/update-inventory.dto'; +import { InventoryQueryDto } from './dtos/inventory-query.dto'; + +@Injectable() +export class InventoryService { + constructor( + @InjectRepository(Inventory) + private readonly inventoryRepository: Repository, + ) {} + + async create(dto: CreateInventoryDto): Promise { + const totalValue = dto.quantity && dto.unitPrice ? dto.quantity * dto.unitPrice : 0; + const item = this.inventoryRepository.create({ ...dto, totalValue }); + return this.inventoryRepository.save(item); + } + + async findAll(query: InventoryQueryDto): Promise<{ data: Inventory[]; total: number }> { + const { page = 1, limit = 20, categoryId, location, search, lowStock } = query; + const qb = this.inventoryRepository.createQueryBuilder('item') + .leftJoinAndSelect('item.asset', 'asset') + .skip((page - 1) * limit) + .take(limit) + .orderBy('item.createdAt', 'DESC'); + + if (categoryId) qb.andWhere('item.categoryId = :categoryId', { categoryId }); + if (location) qb.andWhere('item.location ILIKE :location', { location: `%${location}%` }); + if (search) { + qb.andWhere( + '(item.notes ILIKE :search OR item.location ILIKE :search)', + { search: `%${search}%` }, + ); + } + if (lowStock) { + qb.andWhere('item.quantity <= item.reorderLevel'); + } + + const [data, total] = await qb.getManyAndCount(); + return { data, total }; + } + + async findById(id: string): Promise { + const item = await this.inventoryRepository.findOne({ + where: { id }, + relations: ['asset'], + }); + if (!item) throw new NotFoundException('Inventory item not found'); + return item; + } + + async update(id: string, dto: UpdateInventoryDto): Promise { + const item = await this.findById(id); + Object.assign(item, dto); + if (dto.quantity !== undefined || dto.unitPrice !== undefined) { + item.totalValue = (dto.quantity ?? item.quantity) * (dto.unitPrice ?? item.unitPrice); + } + return this.inventoryRepository.save(item); + } + + async remove(id: string): Promise { + const item = await this.findById(id); + await this.inventoryRepository.softDelete(item.id); + } +} diff --git a/backend/src/vendors/dtos/create-vendor.dto.ts b/backend/src/vendors/dtos/create-vendor.dto.ts new file mode 100644 index 00000000..f0a7d546 --- /dev/null +++ b/backend/src/vendors/dtos/create-vendor.dto.ts @@ -0,0 +1,34 @@ +import { IsString, IsOptional, IsBoolean, IsEmail } from 'class-validator'; + +export class CreateVendorDto { + @IsString() + name: string; + + @IsOptional() + @IsString() + contactPerson?: string; + + @IsOptional() + @IsEmail() + email?: string; + + @IsOptional() + @IsString() + phone?: string; + + @IsOptional() + @IsString() + address?: string; + + @IsOptional() + @IsString() + website?: string; + + @IsOptional() + @IsString() + notes?: string; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/backend/src/vendors/dtos/update-vendor.dto.ts b/backend/src/vendors/dtos/update-vendor.dto.ts new file mode 100644 index 00000000..dd8135d9 --- /dev/null +++ b/backend/src/vendors/dtos/update-vendor.dto.ts @@ -0,0 +1,35 @@ +import { IsString, IsOptional, IsBoolean, IsEmail } from 'class-validator'; + +export class UpdateVendorDto { + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsString() + contactPerson?: string; + + @IsOptional() + @IsEmail() + email?: string; + + @IsOptional() + @IsString() + phone?: string; + + @IsOptional() + @IsString() + address?: string; + + @IsOptional() + @IsString() + website?: string; + + @IsOptional() + @IsString() + notes?: string; + + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/backend/src/vendors/entities/vendor.entity.ts b/backend/src/vendors/entities/vendor.entity.ts new file mode 100644 index 00000000..196dabd9 --- /dev/null +++ b/backend/src/vendors/entities/vendor.entity.ts @@ -0,0 +1,37 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('vendors') +export class Vendor { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column({ nullable: true }) + contactPerson: string; + + @Column({ nullable: true }) + email: string; + + @Column({ nullable: true }) + phone: string; + + @Column({ nullable: true }) + address: string; + + @Column({ nullable: true }) + website: string; + + @Column({ nullable: true, type: 'text' }) + notes: string; + + @Column({ default: true }) + isActive: boolean; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/vendors/vendors.controller.ts b/backend/src/vendors/vendors.controller.ts new file mode 100644 index 00000000..5a19abe9 --- /dev/null +++ b/backend/src/vendors/vendors.controller.ts @@ -0,0 +1,37 @@ +import { Controller, Get, Post, Put, Delete, Param, Body, Query, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; +import { VendorsService } from './vendors.service'; +import { CreateVendorDto } from './dtos/create-vendor.dto'; +import { UpdateVendorDto } from './dtos/update-vendor.dto'; + +@Controller('vendors') +@UseGuards(AuthGuard('jwt')) +export class VendorsController { + constructor(private readonly vendorsService: VendorsService) {} + + @Post() + async create(@Body() dto: CreateVendorDto) { + return this.vendorsService.create(dto); + } + + @Get() + async findAll(@Query() query: { page?: number; limit?: number; isActive?: boolean; search?: string }) { + return this.vendorsService.findAll(query); + } + + @Get(':id') + async findOne(@Param('id') id: string) { + return this.vendorsService.findById(id); + } + + @Put(':id') + async update(@Param('id') id: string, @Body() dto: UpdateVendorDto) { + return this.vendorsService.update(id, dto); + } + + @Delete(':id') + async remove(@Param('id') id: string) { + await this.vendorsService.remove(id); + return { message: 'Vendor deleted successfully' }; + } +} diff --git a/backend/src/vendors/vendors.module.ts b/backend/src/vendors/vendors.module.ts new file mode 100644 index 00000000..12c5a689 --- /dev/null +++ b/backend/src/vendors/vendors.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Vendor } from './entities/vendor.entity'; +import { VendorsService } from './vendors.service'; +import { VendorsController } from './vendors.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Vendor])], + controllers: [VendorsController], + providers: [VendorsService], + exports: [VendorsService], +}) +export class VendorsModule {} diff --git a/backend/src/vendors/vendors.service.ts b/backend/src/vendors/vendors.service.ts new file mode 100644 index 00000000..625e688c --- /dev/null +++ b/backend/src/vendors/vendors.service.ts @@ -0,0 +1,55 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Vendor } from './entities/vendor.entity'; +import { CreateVendorDto } from './dtos/create-vendor.dto'; +import { UpdateVendorDto } from './dtos/update-vendor.dto'; + +@Injectable() +export class VendorsService { + constructor( + @InjectRepository(Vendor) + private readonly vendorRepository: Repository, + ) {} + + async create(dto: CreateVendorDto): Promise { + const vendor = this.vendorRepository.create(dto); + return this.vendorRepository.save(vendor); + } + + async findAll(query: { page?: number; limit?: number; isActive?: boolean; search?: string } = {}): Promise<{ data: Vendor[]; total: number }> { + const { page = 1, limit = 20, isActive, search } = query; + const qb = this.vendorRepository.createQueryBuilder('vendor') + .skip((page - 1) * limit) + .take(limit) + .orderBy('vendor.createdAt', 'DESC'); + + if (isActive !== undefined) qb.andWhere('vendor.isActive = :isActive', { isActive }); + if (search) { + qb.andWhere( + '(vendor.name ILIKE :search OR vendor.email ILIKE :search OR vendor.contactPerson ILIKE :search)', + { search: `%${search}%` }, + ); + } + + const [data, total] = await qb.getManyAndCount(); + return { data, total }; + } + + async findById(id: string): Promise { + const vendor = await this.vendorRepository.findOne({ where: { id } }); + if (!vendor) throw new NotFoundException('Vendor not found'); + return vendor; + } + + async update(id: string, dto: UpdateVendorDto): Promise { + const vendor = await this.findById(id); + Object.assign(vendor, dto); + return this.vendorRepository.save(vendor); + } + + async remove(id: string): Promise { + const vendor = await this.findById(id); + await this.vendorRepository.softDelete(vendor.id); + } +}