diff --git a/backend/src/cost-centers/cost-center.entity.ts b/backend/src/cost-centers/cost-center.entity.ts new file mode 100644 index 0000000..b6e9d87 --- /dev/null +++ b/backend/src/cost-centers/cost-center.entity.ts @@ -0,0 +1,35 @@ +import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { Asset } from '../../assets/entities/asset.entity'; +import { Expense } from '../../expenses/entities/expense.entity'; + + +@Entity({ name: 'cost_centers' }) +export class CostCenter { +@PrimaryGeneratedColumn('uuid') +id: string; + + +@Column({ type: 'varchar', length: 200, unique: true }) +name: string; + + +@Column({ type: 'text', nullable: true }) +description?: string | null; + + +// Optional reverse relations if you maintain assets/expenses in the same project +@OneToMany(() => Asset, (asset) => asset.costCenter, { cascade: false }) +assets?: Asset[]; + + +@OneToMany(() => Expense, (expense) => expense.costCenter, { cascade: false }) +expenses?: Expense[]; + + +@CreateDateColumn({ name: 'created_at' }) +createdAt: Date; + + +@UpdateDateColumn({ name: 'updated_at' }) +updatedAt: Date; +} \ No newline at end of file diff --git a/backend/src/cost-centers/cost-centers.controller.ts b/backend/src/cost-centers/cost-centers.controller.ts new file mode 100644 index 0000000..3fd8776 --- /dev/null +++ b/backend/src/cost-centers/cost-centers.controller.ts @@ -0,0 +1,54 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; +import { CostCentersService } from './cost-centers.service'; +import { CreateCostCenterDto } from './dto/create-cost-center.dto'; +import { UpdateCostCenterDto } from './dto/update-cost-center.dto'; + + +@Controller('cost-centers') +export class CostCentersController { +constructor(private readonly svc: CostCentersService) {} + + +@Post() +create(@Body() dto: CreateCostCenterDto) { +return this.svc.create(dto); +} + + +@Get() +findAll() { +return this.svc.findAll(); +} + + +@Get(':id') +findOne(@Param('id') id: string) { +return this.svc.findOne(id); +} + + +@Patch(':id') +update(@Param('id') id: string, @Body() dto: UpdateCostCenterDto) { +return this.svc.update(id, dto); +} + + +@Delete(':id') +remove(@Param('id') id: string) { +return this.svc.remove(id); +} + + +// Link existing asset to cost center +@Post(':id/assets/:assetId') +attachAsset(@Param('id') id: string, @Param('assetId') assetId: string) { +return this.svc.attachAsset(id, assetId); +} + + +// Link existing expense to cost center +@Post(':id/expenses/:expenseId') +attachExpense(@Param('id') id: string, @Param('expenseId') expenseId: string) { +return this.svc.attachExpense(id, expenseId); +} +} \ No newline at end of file diff --git a/backend/src/cost-centers/cost-centers.service.ts b/backend/src/cost-centers/cost-centers.service.ts new file mode 100644 index 0000000..64bfb12 --- /dev/null +++ b/backend/src/cost-centers/cost-centers.service.ts @@ -0,0 +1,58 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +} + + +async remove(id: string) { +const cc = await this.repo.findOne({ where: { id } }); +if (!cc) throw new NotFoundException('Cost center not found'); +// Optionally: check if assets/expenses exist and prevent deletion +const hasChildren = await this.repo.manager +.getRepository('Asset') +.createQueryBuilder('a') +.where('a.costCenterId = :id', { id }) +.getCount(); + + +if (hasChildren > 0) { +throw new BadRequestException('Cannot delete cost center with linked assets'); +} + + +await this.repo.remove(cc); +return { deleted: true }; +} + + +// Helper: attach existing asset/expense to cost center by id +async attachAsset(costCenterId: string, assetId: string) { +// This method uses query runner / repository to update asset's foreign key +const cc = await this.repo.findOne({ where: { id: costCenterId } }); +if (!cc) throw new NotFoundException('Cost center not found'); + + +const assetRepo = this.repo.manager.getRepository('Asset'); +const asset = await assetRepo.findOne({ where: { id: assetId } }); +if (!asset) throw new NotFoundException('Asset not found'); + + +asset.costCenterId = costCenterId; +await assetRepo.save(asset); +return asset; +} + + +async attachExpense(costCenterId: string, expenseId: string) { +const cc = await this.repo.findOne({ where: { id: costCenterId } }); +if (!cc) throw new NotFoundException('Cost center not found'); + + +const expenseRepo = this.repo.manager.getRepository('Expense'); +const expense = await expenseRepo.findOne({ where: { id: expenseId } }); +if (!expense) throw new NotFoundException('Expense not found'); + + +expense.costCenterId = costCenterId; +await expenseRepo.save(expense); +return expense; +} +} \ No newline at end of file diff --git a/backend/src/cost-centers/cost.module.ts b/backend/src/cost-centers/cost.module.ts new file mode 100644 index 0000000..1ea563e --- /dev/null +++ b/backend/src/cost-centers/cost.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CostCenter } from './entities/cost-center.entity'; +import { CostCentersService } from './cost-centers.service'; +import { CostCentersController } from './cost-centers.controller'; + + +@Module({ +imports: [TypeOrmModule.forFeature([CostCenter])], +providers: [CostCentersService], +controllers: [CostCentersController], +exports: [CostCentersService], +}) +export class CostCentersModule {} \ No newline at end of file diff --git a/backend/src/cost-centers/create-cost-center.dto.ts b/backend/src/cost-centers/create-cost-center.dto.ts new file mode 100644 index 0000000..7a5b4b0 --- /dev/null +++ b/backend/src/cost-centers/create-cost-center.dto.ts @@ -0,0 +1,14 @@ +import { IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator'; + + +export class CreateCostCenterDto { +@IsString() +@IsNotEmpty() +@MaxLength(200) +name: string; + + +@IsOptional() +@IsString() +description?: string; +} \ No newline at end of file