diff --git a/backend/package-lock.json b/backend/package-lock.json index 2594a5f..133bbdf 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,9 +16,12 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", + "@types/multer": "^2.0.0", + "@types/uuid": "^10.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "json2csv": "^6.0.0-alpha.2", + "multer": "^2.0.2", "nestjs-i18n": "^10.5.1", "pdfkit": "^0.17.2", "pg": "^8.11.3", @@ -1723,6 +1726,7 @@ "version": "10.4.20", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.20.tgz", "integrity": "sha512-rh97mX3rimyf4xLMLHuTOBKe6UD8LOJ14VlJ1F/PTd6C6ZK9Ak6EHuJvdaGcSFQhd3ZMBh3I6CuujKGW9pNdIg==", + "license": "MIT", "dependencies": { "body-parser": "1.20.3", "cors": "2.8.5", @@ -1814,12 +1818,6 @@ } } }, - "node_modules/@nestjs/swagger/node_modules/path-to-regexp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", - "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==", - "license": "MIT" - }, "node_modules/@nestjs/testing": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.20.tgz", @@ -1863,6 +1861,19 @@ "typeorm": "^0.3.0" } }, + "node_modules/@nestjs/typeorm/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -2093,7 +2104,6 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -2103,7 +2113,6 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -2144,7 +2153,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -2155,7 +2163,6 @@ "version": "5.0.7", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", - "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -2175,8 +2182,7 @@ "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -2237,14 +2243,21 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } }, "node_modules/@types/node": { "version": "20.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", - "devOptional": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2274,20 +2287,17 @@ "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/send": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -2297,7 +2307,6 @@ "version": "1.15.8", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -2332,6 +2341,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.3", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", @@ -6791,6 +6806,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", @@ -9234,8 +9250,7 @@ "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, "node_modules/unicode-properties": { "version": "1.4.1", @@ -9327,16 +9342,16 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/backend/package.json b/backend/package.json index afa4a48..e251098 100644 --- a/backend/package.json +++ b/backend/package.json @@ -27,9 +27,12 @@ "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.2", + "@types/multer": "^2.0.0", + "@types/uuid": "^10.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "json2csv": "^6.0.0-alpha.2", + "multer": "^2.0.2", "nestjs-i18n": "^10.5.1", "pdfkit": "^0.17.2", "pg": "^8.11.3", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index cab74ce..1728a61 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AssetCategoriesModule } from './asset-categories/asset-categories.module'; @@ -25,7 +26,14 @@ import { SearchModule } from './search/search.module'; username: configService.get('DB_USERNAME', 'postgres'), password: configService.get('DB_PASSWORD', 'password'), database: configService.get('DB_DATABASE', 'manage_assets'), - entities: [AssetCategory, Department, User], + entities: [ + AssetCategory, + Department, + User, + FileUpload, + Asset, + Supplier, + ], synchronize: configService.get('NODE_ENV') !== 'production', // Only for development }), inject: [ConfigService], diff --git a/backend/src/assets/entity/asset.entity.ts b/backend/src/assets/entities/assest.entity.ts similarity index 55% rename from backend/src/assets/entity/asset.entity.ts rename to backend/src/assets/entities/assest.entity.ts index 0e1df50..a2a3e78 100644 --- a/backend/src/assets/entity/asset.entity.ts +++ b/backend/src/assets/entities/assest.entity.ts @@ -2,49 +2,59 @@ import { Entity, PrimaryGeneratedColumn, Column, + ManyToOne, + OneToMany, CreateDateColumn, UpdateDateColumn, - ManyToOne, JoinColumn, } from 'typeorm'; import { Supplier } from '../../suppliers/entities/supplier.entity'; -import { Department } from '../../departments/entities/department.entity'; -import { Category } from '../../categories/entities/category.entity'; +import { Department } from '../../departments/department.entity'; +// import { Category } from '../../categories/entities/category.entity'; +import { FileUpload } from '../../file-uploads/entities/file-upload.entity'; @Entity('assets') export class Asset { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn('uuid') + id: string; @Column({ unique: true }) serialNumber: string; + @Column() + name: string; + + @Column({ type: 'decimal', precision: 12, scale: 2, nullable: true }) + purchaseCost?: number; + @Column({ type: 'date' }) purchaseDate: Date; @Column({ type: 'date', nullable: true }) warrantyEnd?: Date; - @ManyToOne(() => Supplier, { nullable: false }) + @Column({ type: 'text', nullable: true }) + description?: string; + + @Column({ default: true }) + isActive: boolean; + + // Relationships + @ManyToOne(() => Supplier, (supplier) => supplier.files, { nullable: true }) @JoinColumn({ name: 'supplier_id' }) - supplier: Supplier; + supplier?: Supplier; @ManyToOne(() => Department, { nullable: true }) @JoinColumn({ name: 'assigned_department_id' }) assignedDepartment?: Department; - @ManyToOne(() => Category, { nullable: false }) - @JoinColumn({ name: 'category_id' }) - category: Category; - - @Column({ type: 'decimal', precision: 12, scale: 2, nullable: true }) - purchaseCost?: number; - - @Column({ type: 'text', nullable: true }) - description?: string; + // This should be uncommented when the category entity is created + // @ManyToOne(() => Category, { nullable: false }) + // @JoinColumn({ name: 'category_id' }) + // category: Category; - @Column({ default: true }) - isActive: boolean; + @OneToMany(() => FileUpload, (fileUpload) => fileUpload.asset) + files: FileUpload[]; @CreateDateColumn() createdAt: Date; diff --git a/backend/src/file-uploads/dto/create-file-upload.dto.ts b/backend/src/file-uploads/dto/create-file-upload.dto.ts new file mode 100644 index 0000000..51bd6fa --- /dev/null +++ b/backend/src/file-uploads/dto/create-file-upload.dto.ts @@ -0,0 +1,19 @@ +import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator'; +import { FileCategory } from '../entities/file-upload.entity'; + +export class CreateFileUploadDto { + @IsEnum(FileCategory) + category: FileCategory; + + @IsString() + @IsOptional() + description?: string; + + @IsUUID() + @IsOptional() + assetId?: string; + + @IsUUID() + @IsOptional() + supplierId?: string; +} diff --git a/backend/src/file-uploads/entities/file-upload.entity.ts b/backend/src/file-uploads/entities/file-upload.entity.ts new file mode 100644 index 0000000..d2e0c8c --- /dev/null +++ b/backend/src/file-uploads/entities/file-upload.entity.ts @@ -0,0 +1,66 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, +} from 'typeorm'; +import { Asset } from '../../assets/entities/assest.entity'; +import { Supplier } from '../../suppliers/entities/supplier.entity'; + +export enum FileCategory { + PURCHASE_RECEIPT = 'purchase_receipt', + CONTRACT = 'contract', + MANUAL = 'manual', + OTHER = 'other', +} + +@Entity('file_uploads') +export class FileUpload { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + fileName: string; + + @Column() + originalName: string; + + @Column() + mimeType: string; + + @Column() + size: number; + + @Column() + path: string; + + @Column({ + type: 'enum', + enum: FileCategory, + default: FileCategory.OTHER, + }) + category: FileCategory; + + @Column({ nullable: true }) + description: string; + + @ManyToOne(() => Asset, (asset) => asset.files, { nullable: true }) + asset: Asset; + + @Column({ nullable: true }) + assetId: string; + + @ManyToOne(() => Supplier, (supplier) => supplier.files, { nullable: true }) + supplier: Supplier; + + @Column({ nullable: true }) + supplierId: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/backend/src/file-uploads/file-uploads.controller.ts b/backend/src/file-uploads/file-uploads.controller.ts new file mode 100644 index 0000000..a7eaed8 --- /dev/null +++ b/backend/src/file-uploads/file-uploads.controller.ts @@ -0,0 +1,134 @@ +import { + Controller, + Get, + Post, + Param, + Delete, + UseInterceptors, + UploadedFile, + Body, + BadRequestException, + StreamableFile, + Res, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { FileUploadsService } from './file-uploads.service'; +import { CreateFileUploadDto } from './dto/create-file-upload.dto'; +import { diskStorage } from 'multer'; +import { extname } from 'path'; +import { createReadStream } from 'fs'; +import { Response } from 'express'; +import { ApiTags, ApiConsumes, ApiBody } from '@nestjs/swagger'; +import { v4 as uuidv4 } from 'uuid'; + +@ApiTags('File Uploads') +@Controller('file-uploads') +export class FileUploadsController { + constructor(private readonly fileUploadsService: FileUploadsService) {} + + @Post() + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + category: { + type: 'string', + enum: ['purchase_receipt', 'contract', 'manual', 'other'], + }, + description: { + type: 'string', + }, + assetId: { + type: 'string', + }, + supplierId: { + type: 'string', + }, + }, + }, + }) + @UseInterceptors( + FileInterceptor('file', { + storage: diskStorage({ + destination: './uploads', + filename: (req, file, cb) => { + const uniqueId = uuidv4(); + const fileExt = extname(file.originalname); + cb(null, `${uniqueId}${fileExt}`); + }, + }), + fileFilter: (req, file, cb) => { + // Validate file type + const allowedMimeTypes = [ + 'application/pdf', + 'image/jpeg', + 'image/png', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ]; + if (!allowedMimeTypes.includes(file.mimetype)) { + return cb(new BadRequestException('File type not allowed'), false); + } + cb(null, true); + }, + limits: { + fileSize: 5 * 1024 * 1024, // 5MB + }, + }), + ) + async uploadFile( + @UploadedFile() file: Express.Multer.File, + @Body() createFileUploadDto: CreateFileUploadDto, + ) { + if (!file) { + throw new BadRequestException('No file uploaded'); + } + return this.fileUploadsService.create(file, createFileUploadDto); + } + + @Get() + findAll() { + return this.fileUploadsService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.fileUploadsService.findOne(id); + } + + @Get('asset/:assetId') + findByAsset(@Param('assetId') assetId: string) { + return this.fileUploadsService.findByAsset(assetId); + } + + @Get('supplier/:supplierId') + findBySupplier(@Param('supplierId') supplierId: string) { + return this.fileUploadsService.findBySupplier(supplierId); + } + + @Get('download/:id') + async downloadFile( + @Param('id') id: string, + @Res({ passthrough: true }) res: Response, + ): Promise { + const file = await this.fileUploadsService.findOne(id); + const stream = createReadStream(file.path); + + res.set({ + 'Content-Disposition': `attachment; filename="${file.originalName}"`, + 'Content-Type': file.mimeType, + }); + + return new StreamableFile(stream); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.fileUploadsService.remove(id); + } +} diff --git a/backend/src/file-uploads/file-uploads.module.ts b/backend/src/file-uploads/file-uploads.module.ts new file mode 100644 index 0000000..356f882 --- /dev/null +++ b/backend/src/file-uploads/file-uploads.module.ts @@ -0,0 +1,30 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FileUploadsService } from './file-uploads.service'; +import { FileUploadsController } from './file-uploads.controller'; +import { FileUpload } from './entities/file-upload.entity'; +import { MulterModule } from '@nestjs/platform-express'; +import { diskStorage } from 'multer'; +import * as path from 'path'; +import * as fs from 'fs'; + +// Ensure uploads directory exists +const uploadsDir = path.join(process.cwd(), 'uploads'); +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} + +@Module({ + imports: [ + TypeOrmModule.forFeature([FileUpload]), + MulterModule.register({ + storage: diskStorage({ + destination: './uploads', + }), + }), + ], + controllers: [FileUploadsController], + providers: [FileUploadsService], + exports: [FileUploadsService], +}) +export class FileUploadsModule {} diff --git a/backend/src/file-uploads/file-uploads.service.ts b/backend/src/file-uploads/file-uploads.service.ts new file mode 100644 index 0000000..904b953 --- /dev/null +++ b/backend/src/file-uploads/file-uploads.service.ts @@ -0,0 +1,78 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { FileUpload } from './entities/file-upload.entity'; +import { CreateFileUploadDto } from './dto/create-file-upload.dto'; +import * as fs from 'fs'; + +@Injectable() +export class FileUploadsService { + constructor( + @InjectRepository(FileUpload) + private readonly fileUploadRepository: Repository, + ) {} + + async create( + file: Express.Multer.File, + createFileUploadDto: CreateFileUploadDto, + ): Promise { + const fileUpload = new FileUpload(); + fileUpload.fileName = file.filename; + fileUpload.originalName = file.originalname; + fileUpload.mimeType = file.mimetype; + fileUpload.size = file.size; + fileUpload.path = file.path; + fileUpload.category = createFileUploadDto.category; + fileUpload.description = createFileUploadDto.description; + fileUpload.assetId = createFileUploadDto.assetId; + fileUpload.supplierId = createFileUploadDto.supplierId; + + return this.fileUploadRepository.save(fileUpload); + } + + async findAll(): Promise { + return this.fileUploadRepository.find({ + relations: ['asset', 'supplier'], + }); + } + + async findOne(id: string): Promise { + const fileUpload = await this.fileUploadRepository.findOne({ + where: { id }, + relations: ['asset', 'supplier'], + }); + + if (!fileUpload) { + throw new NotFoundException(`File upload with ID "${id}" not found`); + } + + return fileUpload; + } + + async findByAsset(assetId: string): Promise { + return this.fileUploadRepository.find({ + where: { assetId }, + relations: ['asset'], + }); + } + + async findBySupplier(supplierId: string): Promise { + return this.fileUploadRepository.find({ + where: { supplierId }, + relations: ['supplier'], + }); + } + + async remove(id: string): Promise { + const fileUpload = await this.findOne(id); + + // Delete the physical file + try { + await fs.promises.unlink(fileUpload.path); + } catch (error) { + console.error(`Error deleting file ${fileUpload.path}:`, error); + } + + await this.fileUploadRepository.remove(fileUpload); + } +} diff --git a/backend/src/suppliers/entities/supplier.entity.ts b/backend/src/suppliers/entities/supplier.entity.ts index f2c2e30..1cc28c9 100644 --- a/backend/src/suppliers/entities/supplier.entity.ts +++ b/backend/src/suppliers/entities/supplier.entity.ts @@ -1,4 +1,6 @@ import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; +import { FileUpload } from '../../file-uploads/entities/file-upload.entity'; +import { Asset } from 'src/assets/entities/assest.entity'; @Entity('suppliers') export class Supplier { @@ -21,6 +23,9 @@ export class Supplier { phone: string; // This should be uncommented when the Asset entity is created - // @OneToMany(() => Asset, (asset) => asset.supplier) - // assets: Asset[]; + @OneToMany(() => Asset, (asset) => asset.supplier) + assets: Asset[]; + + @OneToMany(() => FileUpload, (fileUpload) => fileUpload.supplier) + files: FileUpload[]; }