diff --git a/backend/package-lock.json b/backend/package-lock.json index 141159c..564371e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/mapped-types": "^2.1.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.0", "class-transformer": "^0.5.1", @@ -1686,6 +1687,25 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.20.tgz", diff --git a/backend/package.json b/backend/package.json index 4459e27..7270400 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,7 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/mapped-types": "^2.1.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/typeorm": "^10.0.0", "class-transformer": "^0.5.1", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 0c2a507..cccaa7d 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -7,6 +7,8 @@ import { AssetCategoriesModule } from './asset-categories/asset-categories.modul import { AssetCategory } from './asset-categories/asset-category.entity'; import { DepartmentsModule } from './departments/departments.module'; import { Department } from './departments/department.entity'; +import { AssetSubcategoriesModule } from './asset-subcategories/asset-subcategories.module'; +import { AssetSubcategory } from './asset-subcategories/entities/asset-subcategory.entity'; @Module({ imports: [ @@ -14,23 +16,7 @@ import { Department } from './departments/department.entity'; isGlobal: true, }), TypeOrmModule.forRootAsync({ - imports: [ConfigModule, - // --- ADD THIS CONFIGURATION --- - I18nModule.forRoot({ - fallbackLanguage: 'en', - loaderOptions: { - path: path.join(__dirname, '/i18n/'), // Directory for translation files - watch: true, // Watch for changes in translation files - }, - resolvers: [ - // Order matters: checks query param, then header, then browser settings - new QueryResolver(['lang', 'l']), - new HeaderResolver(['x-custom-lang-header']), - AcceptLanguageResolver, // Standard 'Accept-Language' header - ], - }), - // --- END OF CONFIGURATION --- - ],], + imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ type: 'postgres', host: configService.get('DB_HOST', 'localhost'), @@ -38,12 +24,13 @@ import { Department } from './departments/department.entity'; username: configService.get('DB_USERNAME', 'postgres'), password: configService.get('DB_PASSWORD', 'password'), database: configService.get('DB_DATABASE', 'manage_assets'), - entities: [AssetCategory, Department], + entities: [AssetCategory, AssetSubcategory, Department], synchronize: configService.get('NODE_ENV') !== 'production', // Only for development }), inject: [ConfigService], }), AssetCategoriesModule, + AssetSubcategoriesModule, DepartmentsModule, ], controllers: [AppController], diff --git a/backend/src/asset-categories/asset-category.entity.ts b/backend/src/asset-categories/asset-category.entity.ts index a6ff83d..5908e9d 100644 --- a/backend/src/asset-categories/asset-category.entity.ts +++ b/backend/src/asset-categories/asset-category.entity.ts @@ -1,4 +1,5 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; +import { AssetSubcategory } from '../asset-subcategories/entities/asset-subcategory.entity'; @Entity('asset_categories') export class AssetCategory { @@ -17,8 +18,7 @@ export class AssetCategory { @UpdateDateColumn() updatedAt: Date; - // Relationship with assets (one-to-many) - // This will be uncommented when the Asset entity is created - // @OneToMany(() => Asset, asset => asset.category) - // assets: Asset[]; + // Relationship with subcategories + @OneToMany(() => AssetSubcategory, subcategory => subcategory.parentCategory) + subcategories: AssetSubcategory[]; } diff --git a/backend/src/asset-subcategories/asset-subcategories.controller.ts b/backend/src/asset-subcategories/asset-subcategories.controller.ts new file mode 100644 index 0000000..bcd7c3c --- /dev/null +++ b/backend/src/asset-subcategories/asset-subcategories.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Get, Post, Body, Param, Patch, Delete } from '@nestjs/common'; +import { AssetSubcategoriesService } from './asset-subcategories.service'; +import { CreateAssetSubcategoryDto } from './dto/create-asset-subcategory.dto'; +import { UpdateAssetSubcategoryDto } from './dto/update-asset-subcategory.dto'; + +@Controller('asset-subcategories') +export class AssetSubcategoriesController { + constructor(private readonly service: AssetSubcategoriesService) {} + + @Post() + create(@Body() dto: CreateAssetSubcategoryDto) { + return this.service.create(dto); + } + + @Get() + findAll() { + return this.service.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.service.findOne(Number(id)); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateAssetSubcategoryDto) { + return this.service.update(Number(id), dto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.service.remove(Number(id)); + } +} diff --git a/backend/src/asset-subcategories/asset-subcategories.module.ts b/backend/src/asset-subcategories/asset-subcategories.module.ts new file mode 100644 index 0000000..25a1599 --- /dev/null +++ b/backend/src/asset-subcategories/asset-subcategories.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AssetSubcategory } from './entities/asset-subcategory.entity'; +import { AssetSubcategoriesService } from './asset-subcategories.service'; +import { AssetSubcategoriesController } from './asset-subcategories.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([AssetSubcategory])], + providers: [AssetSubcategoriesService], + controllers: [AssetSubcategoriesController], +}) +export class AssetSubcategoriesModule {} diff --git a/backend/src/asset-subcategories/asset-subcategories.service.ts b/backend/src/asset-subcategories/asset-subcategories.service.ts new file mode 100644 index 0000000..7c2ac2a --- /dev/null +++ b/backend/src/asset-subcategories/asset-subcategories.service.ts @@ -0,0 +1,48 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AssetSubcategory } from './entities/asset-subcategory.entity'; +import { CreateAssetSubcategoryDto } from './dto/create-asset-subcategory.dto'; +import { UpdateAssetSubcategoryDto } from './dto/update-asset-subcategory.dto'; +import { AssetCategory } from '../asset-categories/asset-category.entity'; + +@Injectable() +export class AssetSubcategoriesService { + constructor( + @InjectRepository(AssetSubcategory) + private readonly subcategoryRepo: Repository, + @InjectRepository(AssetCategory) + private readonly categoryRepo: Repository, + ) {} + + async create(dto: CreateAssetSubcategoryDto): Promise { + const parentCategory = await this.categoryRepo.findOne({ where: { id: dto.parentCategoryId } }); + if (!parentCategory) throw new NotFoundException('Parent category not found'); + const subcategory = this.subcategoryRepo.create({ ...dto, parentCategory }); + return this.subcategoryRepo.save(subcategory); + } + + findAll(): Promise { + return this.subcategoryRepo.find({ relations: ['parentCategory'] }); + } + + findOne(id: number): Promise { + return this.subcategoryRepo.findOne({ where: { id }, relations: ['parentCategory'] }); + } + + async update(id: number, dto: UpdateAssetSubcategoryDto): Promise { + const subcategory = await this.subcategoryRepo.findOne({ where: { id } }); + if (!subcategory) throw new NotFoundException('Subcategory not found'); + if (dto.parentCategoryId) { + const parentCategory = await this.categoryRepo.findOne({ where: { id: dto.parentCategoryId } }); + if (!parentCategory) throw new NotFoundException('Parent category not found'); + subcategory.parentCategory = parentCategory; + } + if (dto.name) subcategory.name = dto.name; + return this.subcategoryRepo.save(subcategory); + } + + async remove(id: number): Promise { + await this.subcategoryRepo.delete(id); + } +} diff --git a/backend/src/asset-subcategories/dto/create-asset-subcategory.dto.ts b/backend/src/asset-subcategories/dto/create-asset-subcategory.dto.ts new file mode 100644 index 0000000..ae26bf0 --- /dev/null +++ b/backend/src/asset-subcategories/dto/create-asset-subcategory.dto.ts @@ -0,0 +1,11 @@ +import { IsInt, IsNotEmpty, IsString } from 'class-validator'; + +export class CreateAssetSubcategoryDto { + @IsNotEmpty() + @IsString() + name: string; + + @IsNotEmpty() + @IsInt() + parentCategoryId: number; +} diff --git a/backend/src/asset-subcategories/dto/update-asset-subcategory.dto.ts b/backend/src/asset-subcategories/dto/update-asset-subcategory.dto.ts new file mode 100644 index 0000000..806121f --- /dev/null +++ b/backend/src/asset-subcategories/dto/update-asset-subcategory.dto.ts @@ -0,0 +1,7 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateAssetSubcategoryDto } from './create-asset-subcategory.dto'; + +export class UpdateAssetSubcategoryDto extends PartialType(CreateAssetSubcategoryDto) { + name?: string; + parentCategoryId?: number; +} diff --git a/backend/src/asset-subcategories/entities/asset-subcategory.entity.ts b/backend/src/asset-subcategories/entities/asset-subcategory.entity.ts new file mode 100644 index 0000000..9931563 --- /dev/null +++ b/backend/src/asset-subcategories/entities/asset-subcategory.entity.ts @@ -0,0 +1,14 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { AssetCategory } from '../../asset-categories/asset-category.entity'; + +@Entity('asset_subcategories') +export class AssetSubcategory { + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @ManyToOne(() => AssetCategory, category => category.subcategories, { nullable: false }) + parentCategory: AssetCategory; +} diff --git a/backend/src/inventory/entities/inventory-item.entity.ts b/backend/src/inventory/entities/inventory-item.entity.ts index 79a4587..6973891 100644 --- a/backend/src/inventory/entities/inventory-item.entity.ts +++ b/backend/src/inventory/entities/inventory-item.entity.ts @@ -1,4 +1,6 @@ import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { ManyToOne } from 'typeorm'; +import { AssetSubcategory } from '../../asset-subcategories/entities/asset-subcategory.entity'; @Entity() export class InventoryItem { @@ -17,4 +19,7 @@ export class InventoryItem { // --- ADD THIS NEW FIELD --- @Column({ type: 'int', default: 10 }) // Default threshold of 10 units threshold: number; + + @ManyToOne(() => AssetSubcategory, { nullable: true }) + subcategory: AssetSubcategory; } \ No newline at end of file