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
9 changes: 5 additions & 4 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { EmailModule } from './email/email.module';
import { SuppliersModule } from './suppliers/suppliers.module';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/guards/jwt.guard';
import { AssetDepreciationModule } from './asset-depreciation/asset-depreciation.module';
Expand Down Expand Up @@ -33,10 +34,10 @@ import { AssetDepreciationModule } from './asset-depreciation/asset-depreciation
: false,
}),
}),
AuthModule,
UsersModule,
EmailModule,
AssetDepreciationModule,
AuthModule,
UsersModule,
EmailModule,
SuppliersModule,
],
controllers: [AppController],
providers: [
Expand Down
17 changes: 17 additions & 0 deletions backend/src/assets/assets.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { Supplier } from '../suppliers/suppliers.entity';

@Entity('assets')
export class Asset {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

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

@ManyToOne(() => Supplier, supplier => supplier.assets)
supplier: Supplier;
}
9 changes: 9 additions & 0 deletions backend/src/suppliers/dto/assign-asset.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsInt } from 'class-validator';

export class AssignAssetDto {
@IsInt()
supplierId: number;

@IsInt()
assetId: number;
}
23 changes: 23 additions & 0 deletions backend/src/suppliers/dto/create-supplier.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IsString, IsOptional, IsEmail, IsNotEmpty } from 'class-validator';

export class CreateSupplierDto {
@IsString()
@IsNotEmpty()
name: string;

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

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

@IsEmail()
@IsNotEmpty()
email: string;

@IsString()
@IsOptional()
phone?: string;
}
17 changes: 17 additions & 0 deletions backend/src/suppliers/dto/query-supplier.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { IsOptional, IsString, IsEmail } from 'class-validator';

export class QuerySupplierDto {
@IsOptional()
@IsString()
name?: string;

@IsOptional()
@IsEmail()
email?: string;

@IsOptional()
page?: number;

@IsOptional()
limit?: number;
}
4 changes: 4 additions & 0 deletions backend/src/suppliers/dto/update-supplier.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateSupplierDto } from './create-supplier.dto';

export class UpdateSupplierDto extends PartialType(CreateSupplierDto) {}
70 changes: 70 additions & 0 deletions backend/src/suppliers/suppliers.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SuppliersController } from './suppliers.controller';
import { SuppliersService } from './suppliers.service';

const supplier = { id: 1, name: 'Supplier1', email: 's1@email.com', isActive: true, assets: [] };

describe('SuppliersController', () => {
let controller: SuppliersController;
let service: SuppliersService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SuppliersController],
providers: [
{
provide: SuppliersService,
useValue: {
create: jest.fn().mockResolvedValue(supplier),
findAll: jest.fn().mockResolvedValue({ data: [supplier], total: 1 }),
findOne: jest.fn().mockResolvedValue(supplier),
update: jest.fn().mockResolvedValue({ ...supplier, name: 'Updated' }),
remove: jest.fn().mockResolvedValue(undefined),
assignAsset: jest.fn().mockResolvedValue(supplier),
unassignAsset: jest.fn().mockResolvedValue(supplier),
toggleStatus: jest.fn().mockResolvedValue({ ...supplier, isActive: false }),
},
},
],
}).compile();

controller = module.get<SuppliersController>(SuppliersController);
service = module.get<SuppliersService>(SuppliersService);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

it('should create a supplier', async () => {
expect(await controller.create({ name: 'Supplier1', email: 's1@email.com' })).toEqual(supplier);
});

it('should get all suppliers', async () => {
expect(await controller.findAll({})).toEqual({ data: [supplier], total: 1 });
});

it('should get one supplier', async () => {
expect(await controller.findOne('1')).toEqual(supplier);
});

it('should update a supplier', async () => {
expect(await controller.update('1', { name: 'Updated' })).toHaveProperty('name', 'Updated');
});

it('should remove a supplier', async () => {
await expect(controller.remove('1')).resolves.toBeUndefined();
});

it('should assign asset', async () => {
expect(await controller.assignAsset({ supplierId: 1, assetId: 2 })).toEqual(supplier);
});

it('should unassign asset', async () => {
expect(await controller.unassignAsset({ supplierId: 1, assetId: 2 })).toEqual(supplier);
});

it('should toggle status', async () => {
expect(await controller.toggleStatus('1')).toHaveProperty('isActive', false);
});
});
47 changes: 47 additions & 0 deletions backend/src/suppliers/suppliers.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// ...existing code...
// ...existing code...
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';
import { SuppliersService } from './suppliers.service';
import { CreateSupplierDto } from './dto/create-supplier.dto';
import { UpdateSupplierDto } from './dto/update-supplier.dto';
import { QuerySupplierDto } from './dto/query-supplier.dto';
import { AssignAssetDto } from './dto/assign-asset.dto';

@Controller('suppliers')
export class SuppliersController {
constructor(private readonly suppliersService: SuppliersService) {}

@Post()
create(@Body() createSupplierDto: CreateSupplierDto) {
return this.suppliersService.create(createSupplierDto);
}

@Get()
findAll(@Query() query: QuerySupplierDto) {
return this.suppliersService.findAll(query);
}
@Post('assign-asset')
assignAsset(@Body() assignAssetDto: AssignAssetDto) {
return this.suppliersService.assignAsset(assignAssetDto.supplierId, assignAssetDto.assetId);
}

@Post('unassign-asset')
unassignAsset(@Body() assignAssetDto: AssignAssetDto) {
return this.suppliersService.unassignAsset(assignAssetDto.supplierId, assignAssetDto.assetId);
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.suppliersService.findOne(+id);
}

@Patch(':id')
update(@Param('id') id: string, @Body() updateSupplierDto: UpdateSupplierDto) {
return this.suppliersService.update(+id, updateSupplierDto);
}

@Delete(':id')
remove(@Param('id') id: string) {
return this.suppliersService.remove(+id);
}
}
38 changes: 38 additions & 0 deletions backend/src/suppliers/suppliers.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
import { Asset } from '../assets/assets.entity';

@Entity('suppliers')
export class Supplier {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

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

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

@Column({ unique: true })
email: string;

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

@Column({ default: true })
isActive: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;

@DeleteDateColumn()
deletedAt?: Date;

@OneToMany(() => Asset, asset => asset.supplier)
assets: Asset[];
}
12 changes: 12 additions & 0 deletions backend/src/suppliers/suppliers.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SuppliersService } from './suppliers.service';
import { SuppliersController } from './suppliers.controller';
import { Supplier } from './suppliers.entity';

@Module({
imports: [TypeOrmModule.forFeature([Supplier])],
controllers: [SuppliersController],
providers: [SuppliersService],
})
export class SuppliersModule {}
71 changes: 71 additions & 0 deletions backend/src/suppliers/suppliers.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Supplier } from './suppliers.entity';
import { CreateSupplierDto } from './dto/create-supplier.dto';
import { UpdateSupplierDto } from './dto/update-supplier.dto';
import { QuerySupplierDto } from './dto/query-supplier.dto';
import { Asset } from '../assets/assets.entity';

@Injectable()
export class SuppliersService {
constructor(
@InjectRepository(Supplier)
private suppliersRepository: Repository<Supplier>,
) {}

create(createSupplierDto: CreateSupplierDto): Promise<Supplier> {
const supplier = this.suppliersRepository.create(createSupplierDto);
return this.suppliersRepository.save(supplier);
}

async findAll(query: QuerySupplierDto): Promise<{ data: Supplier[]; total: number }> {
const { name, email, page = 1, limit = 10 } = query;
const qb = this.suppliersRepository.createQueryBuilder('supplier').leftJoinAndSelect('supplier.assets', 'asset');
if (name) qb.andWhere('supplier.name ILIKE :name', { name: `%${name}%` });
if (email) qb.andWhere('supplier.email = :email', { email });
const [data, total] = await qb.skip((page - 1) * limit).take(limit).getManyAndCount();
return { data, total };
}

async toggleStatus(id: number): Promise<Supplier> {
const supplier = await this.findOne(id);
supplier.isActive = !supplier.isActive;
return this.suppliersRepository.save(supplier);
}
async findOne(id: number): Promise<Supplier> {
const supplier = await this.suppliersRepository.findOne({ where: { id }, relations: ['assets'] });
if (!supplier) throw new NotFoundException('Supplier not found');
return supplier;
}

async update(id: number, updateSupplierDto: UpdateSupplierDto): Promise<Supplier> {
const supplier = await this.findOne(id);
Object.assign(supplier, updateSupplierDto);
return this.suppliersRepository.save(supplier);
}

async remove(id: number): Promise<void> {
await this.suppliersRepository.softDelete(id);
}

async assignAsset(supplierId: number, assetId: number): Promise<Supplier> {
const supplier = await this.findOne(supplierId);
const assetRepo = this.suppliersRepository.manager.getRepository(Asset);
const asset = await assetRepo.findOne({ where: { id: assetId } });
if (!asset) throw new NotFoundException('Asset not found');
asset.supplier = supplier;
await assetRepo.save(asset);
return this.findOne(supplierId);
}

async unassignAsset(supplierId: number, assetId: number): Promise<Supplier> {
const supplier = await this.findOne(supplierId);
const assetRepo = this.suppliersRepository.manager.getRepository(Asset);
const asset = await assetRepo.findOne({ where: { id: assetId, supplier: { id: supplierId } } });
if (!asset) throw new NotFoundException('Asset not found for this supplier');
asset.supplier = null;
await assetRepo.save(asset);
return this.findOne(supplierId);
}
}
Loading