251122:1700 Phase 4
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Delete,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { ContractDrawingService } from './contract-drawing.service';
|
||||
import { CreateContractDrawingDto } from './dto/create-contract-drawing.dto';
|
||||
import { UpdateContractDrawingDto } from './dto/update-contract-drawing.dto';
|
||||
import { SearchContractDrawingDto } from './dto/search-contract-drawing.dto';
|
||||
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
@ApiTags('Contract Drawings')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
@Controller('drawings/contract')
|
||||
export class ContractDrawingController {
|
||||
constructor(
|
||||
private readonly contractDrawingService: ContractDrawingService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create new Contract Drawing' })
|
||||
@RequirePermission('drawing.create') // สิทธิ์ ID 39: สร้าง/แก้ไขข้อมูลแบบ
|
||||
create(
|
||||
@Body() createDto: CreateContractDrawingDto,
|
||||
@CurrentUser() user: User,
|
||||
) {
|
||||
return this.contractDrawingService.create(createDto, user);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Search Contract Drawings' })
|
||||
@RequirePermission('document.view') // สิทธิ์ ID 31: ดูเอกสารทั่วไป
|
||||
findAll(@Query() searchDto: SearchContractDrawingDto) {
|
||||
return this.contractDrawingService.findAll(searchDto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Contract Drawing details' })
|
||||
@RequirePermission('document.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.contractDrawingService.findOne(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@ApiOperation({ summary: 'Update Contract Drawing' })
|
||||
@RequirePermission('drawing.create') // สิทธิ์ ID 39 ครอบคลุมการแก้ไขด้วย
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateDto: UpdateContractDrawingDto,
|
||||
@CurrentUser() user: User,
|
||||
) {
|
||||
return this.contractDrawingService.update(id, updateDto, user);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete Contract Drawing (Soft Delete)' })
|
||||
@RequirePermission('document.delete') // สิทธิ์ ID 34: ลบเอกสาร
|
||||
remove(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User) {
|
||||
return this.contractDrawingService.remove(id, user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, In, Brackets } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { ContractDrawing } from './entities/contract-drawing.entity';
|
||||
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
// DTOs
|
||||
import { CreateContractDrawingDto } from './dto/create-contract-drawing.dto';
|
||||
import { SearchContractDrawingDto } from './dto/search-contract-drawing.dto';
|
||||
import { UpdateContractDrawingDto } from './dto/update-contract-drawing.dto';
|
||||
|
||||
// Services
|
||||
import { FileStorageService } from '../../common/file-storage/file-storage.service';
|
||||
|
||||
@Injectable()
|
||||
export class ContractDrawingService {
|
||||
private readonly logger = new Logger(ContractDrawingService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ContractDrawing)
|
||||
private drawingRepo: Repository<ContractDrawing>,
|
||||
@InjectRepository(Attachment)
|
||||
private attachmentRepo: Repository<Attachment>,
|
||||
private fileStorageService: FileStorageService,
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* สร้างแบบสัญญาใหม่ (Create Contract Drawing)
|
||||
* - ตรวจสอบเลขที่ซ้ำในโปรเจกต์
|
||||
* - บันทึกข้อมูล
|
||||
* - ผูกไฟล์แนบและ Commit ไฟล์จาก Temp -> Permanent
|
||||
*/
|
||||
async create(createDto: CreateContractDrawingDto, user: User) {
|
||||
// 1. ตรวจสอบเลขที่แบบซ้ำ (Unique per Project)
|
||||
const exists = await this.drawingRepo.findOne({
|
||||
where: {
|
||||
projectId: createDto.projectId,
|
||||
contractDrawingNo: createDto.contractDrawingNo,
|
||||
},
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
throw new ConflictException(
|
||||
`Contract Drawing No. "${createDto.contractDrawingNo}" already exists in this project.`,
|
||||
);
|
||||
}
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 2. เตรียมไฟล์แนบ
|
||||
let attachments: Attachment[] = [];
|
||||
if (createDto.attachmentIds?.length) {
|
||||
attachments = await this.attachmentRepo.findBy({
|
||||
id: In(createDto.attachmentIds),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. สร้าง Entity
|
||||
const drawing = queryRunner.manager.create(ContractDrawing, {
|
||||
projectId: createDto.projectId,
|
||||
contractDrawingNo: createDto.contractDrawingNo,
|
||||
title: createDto.title,
|
||||
subCategoryId: createDto.subCategoryId,
|
||||
volumeId: createDto.volumeId,
|
||||
updatedBy: user.user_id,
|
||||
attachments: attachments,
|
||||
});
|
||||
|
||||
const savedDrawing = await queryRunner.manager.save(drawing);
|
||||
|
||||
// 4. Commit Files (ย้ายไฟล์จริง)
|
||||
if (createDto.attachmentIds?.length) {
|
||||
// ✅ FIX TS2345: แปลง number[] เป็น string[] ก่อนส่ง
|
||||
await this.fileStorageService.commit(
|
||||
createDto.attachmentIds.map(String),
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return savedDrawing;
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
// ✅ FIX TS18046: Cast err เป็น Error
|
||||
this.logger.error(
|
||||
`Failed to create contract drawing: ${(err as Error).message}`,
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ค้นหาแบบสัญญา (Search & Filter)
|
||||
* รองรับ Pagination และการค้นหาด้วย Text
|
||||
*/
|
||||
async findAll(searchDto: SearchContractDrawingDto) {
|
||||
const {
|
||||
projectId,
|
||||
volumeId,
|
||||
subCategoryId,
|
||||
search,
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
} = searchDto;
|
||||
|
||||
const query = this.drawingRepo
|
||||
.createQueryBuilder('drawing')
|
||||
.leftJoinAndSelect('drawing.attachments', 'files')
|
||||
// .leftJoinAndSelect('drawing.subCategory', 'subCat')
|
||||
// .leftJoinAndSelect('drawing.volume', 'vol')
|
||||
.where('drawing.projectId = :projectId', { projectId });
|
||||
|
||||
// Filter by Volume
|
||||
if (volumeId) {
|
||||
query.andWhere('drawing.volumeId = :volumeId', { volumeId });
|
||||
}
|
||||
|
||||
// Filter by SubCategory
|
||||
if (subCategoryId) {
|
||||
query.andWhere('drawing.subCategoryId = :subCategoryId', {
|
||||
subCategoryId,
|
||||
});
|
||||
}
|
||||
|
||||
// Search Text (No. or Title)
|
||||
if (search) {
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where('drawing.contractDrawingNo LIKE :search', {
|
||||
search: `%${search}%`,
|
||||
}).orWhere('drawing.title LIKE :search', { search: `%${search}%` });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
query.orderBy('drawing.contractDrawingNo', 'ASC');
|
||||
|
||||
const skip = (page - 1) * pageSize;
|
||||
query.skip(skip).take(pageSize);
|
||||
|
||||
const [items, total] = await query.getManyAndCount();
|
||||
|
||||
return {
|
||||
data: items,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ดึงข้อมูลแบบรายตัว (Get One)
|
||||
*/
|
||||
async findOne(id: number) {
|
||||
const drawing = await this.drawingRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['attachments'], // เพิ่ม relations อื่นๆ ตามต้องการ
|
||||
});
|
||||
|
||||
if (!drawing) {
|
||||
throw new NotFoundException(`Contract Drawing ID ${id} not found`);
|
||||
}
|
||||
|
||||
return drawing;
|
||||
}
|
||||
|
||||
/**
|
||||
* แก้ไขข้อมูลแบบ (Update)
|
||||
*/
|
||||
async update(id: number, updateDto: UpdateContractDrawingDto, user: User) {
|
||||
const drawing = await this.findOne(id);
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// Update Fields
|
||||
if (updateDto.contractDrawingNo)
|
||||
drawing.contractDrawingNo = updateDto.contractDrawingNo;
|
||||
if (updateDto.title) drawing.title = updateDto.title;
|
||||
if (updateDto.volumeId !== undefined)
|
||||
drawing.volumeId = updateDto.volumeId;
|
||||
if (updateDto.subCategoryId !== undefined)
|
||||
drawing.subCategoryId = updateDto.subCategoryId;
|
||||
|
||||
drawing.updatedBy = user.user_id;
|
||||
|
||||
// Update Attachments (Replace logic)
|
||||
if (updateDto.attachmentIds) {
|
||||
const newAttachments = await this.attachmentRepo.findBy({
|
||||
id: In(updateDto.attachmentIds),
|
||||
});
|
||||
drawing.attachments = newAttachments;
|
||||
|
||||
// Commit new files
|
||||
// ✅ FIX TS2345: แปลง number[] เป็น string[] ก่อนส่ง
|
||||
await this.fileStorageService.commit(
|
||||
updateDto.attachmentIds.map(String),
|
||||
);
|
||||
}
|
||||
|
||||
const updatedDrawing = await queryRunner.manager.save(drawing);
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return updatedDrawing;
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
// ✅ FIX TS18046: Cast err เป็น Error (Optional: Added logger here too for consistency)
|
||||
this.logger.error(
|
||||
`Failed to update contract drawing: ${(err as Error).message}`,
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบแบบสัญญา (Soft Delete)
|
||||
*/
|
||||
async remove(id: number, user: User) {
|
||||
const drawing = await this.findOne(id);
|
||||
|
||||
// บันทึกว่าใครเป็นคนลบก่อน Soft Delete (Optional)
|
||||
drawing.updatedBy = user.user_id;
|
||||
await this.drawingRepo.save(drawing);
|
||||
|
||||
return this.drawingRepo.softRemove(drawing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { DrawingMasterDataService } from './drawing-master-data.service';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
|
||||
@ApiTags('Drawing Master Data')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
@Controller('drawings/master')
|
||||
export class DrawingMasterDataController {
|
||||
// ✅ ต้องมี export ตรงนี้
|
||||
constructor(private readonly masterDataService: DrawingMasterDataService) {}
|
||||
|
||||
// --- Contract Drawing Endpoints ---
|
||||
|
||||
@Get('contract/volumes')
|
||||
@ApiOperation({ summary: 'List Contract Drawing Volumes' })
|
||||
@RequirePermission('document.view')
|
||||
getVolumes(@Query('projectId', ParseIntPipe) projectId: number) {
|
||||
return this.masterDataService.findAllVolumes(projectId);
|
||||
}
|
||||
|
||||
@Post('contract/volumes')
|
||||
@ApiOperation({ summary: 'Create Volume (Admin/PM)' })
|
||||
@RequirePermission('master_data.drawing_category.manage') // สิทธิ์ ID 16
|
||||
createVolume(@Body() body: any) {
|
||||
// ควรใช้ DTO จริงในการผลิต
|
||||
return this.masterDataService.createVolume(body);
|
||||
}
|
||||
|
||||
@Get('contract/sub-categories')
|
||||
@ApiOperation({ summary: 'List Contract Drawing Sub-Categories' })
|
||||
@RequirePermission('document.view')
|
||||
getContractSubCats(@Query('projectId', ParseIntPipe) projectId: number) {
|
||||
return this.masterDataService.findAllContractSubCats(projectId);
|
||||
}
|
||||
|
||||
@Post('contract/sub-categories')
|
||||
@ApiOperation({ summary: 'Create Contract Sub-Category (Admin/PM)' })
|
||||
@RequirePermission('master_data.drawing_category.manage')
|
||||
createContractSubCat(@Body() body: any) {
|
||||
return this.masterDataService.createContractSubCat(body);
|
||||
}
|
||||
|
||||
// --- Shop Drawing Endpoints ---
|
||||
|
||||
@Get('shop/main-categories')
|
||||
@ApiOperation({ summary: 'List Shop Drawing Main Categories' })
|
||||
@RequirePermission('document.view')
|
||||
getShopMainCats() {
|
||||
return this.masterDataService.findAllShopMainCats();
|
||||
}
|
||||
|
||||
@Get('shop/sub-categories')
|
||||
@ApiOperation({ summary: 'List Shop Drawing Sub-Categories' })
|
||||
@RequirePermission('document.view')
|
||||
getShopSubCats(@Query('mainCategoryId') mainCategoryId?: number) {
|
||||
return this.masterDataService.findAllShopSubCats(mainCategoryId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, FindOptionsWhere } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { ContractDrawingVolume } from './entities/contract-drawing-volume.entity';
|
||||
import { ContractDrawingSubCategory } from './entities/contract-drawing-sub-category.entity';
|
||||
import { ShopDrawingMainCategory } from './entities/shop-drawing-main-category.entity';
|
||||
import { ShopDrawingSubCategory } from './entities/shop-drawing-sub-category.entity';
|
||||
|
||||
@Injectable()
|
||||
export class DrawingMasterDataService {
|
||||
constructor(
|
||||
@InjectRepository(ContractDrawingVolume)
|
||||
private cdVolumeRepo: Repository<ContractDrawingVolume>,
|
||||
@InjectRepository(ContractDrawingSubCategory)
|
||||
private cdSubCatRepo: Repository<ContractDrawingSubCategory>,
|
||||
@InjectRepository(ShopDrawingMainCategory)
|
||||
private sdMainCatRepo: Repository<ShopDrawingMainCategory>,
|
||||
@InjectRepository(ShopDrawingSubCategory)
|
||||
private sdSubCatRepo: Repository<ShopDrawingSubCategory>,
|
||||
) {}
|
||||
|
||||
// --- Contract Drawing Volumes ---
|
||||
async findAllVolumes(projectId: number) {
|
||||
return this.cdVolumeRepo.find({
|
||||
where: { projectId },
|
||||
order: { sortOrder: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async createVolume(data: Partial<ContractDrawingVolume>) {
|
||||
const volume = this.cdVolumeRepo.create(data);
|
||||
return this.cdVolumeRepo.save(volume);
|
||||
}
|
||||
|
||||
// --- Contract Drawing Sub-Categories ---
|
||||
async findAllContractSubCats(projectId: number) {
|
||||
return this.cdSubCatRepo.find({
|
||||
where: { projectId },
|
||||
order: { sortOrder: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async createContractSubCat(data: Partial<ContractDrawingSubCategory>) {
|
||||
const subCat = this.cdSubCatRepo.create(data);
|
||||
return this.cdSubCatRepo.save(subCat);
|
||||
}
|
||||
|
||||
// --- Shop Drawing Main Categories ---
|
||||
async findAllShopMainCats() {
|
||||
return this.sdMainCatRepo.find({
|
||||
where: { isActive: true },
|
||||
order: { sortOrder: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
// --- Shop Drawing Sub Categories ---
|
||||
async findAllShopSubCats(mainCategoryId?: number) {
|
||||
// ✅ FIX: ใช้วิธี Spread Operator เพื่อสร้าง Object เงื่อนไขที่ถูกต้องตาม Type
|
||||
const where: FindOptionsWhere<ShopDrawingSubCategory> = {
|
||||
isActive: true,
|
||||
...(mainCategoryId ? { mainCategoryId } : {}),
|
||||
};
|
||||
|
||||
return this.sdSubCatRepo.find({
|
||||
where,
|
||||
order: { sortOrder: 'ASC' },
|
||||
relations: ['mainCategory'], // Load Parent Info
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
// Entities (Main)
|
||||
import { ContractDrawing } from './entities/contract-drawing.entity';
|
||||
import { ShopDrawing } from './entities/shop-drawing.entity';
|
||||
import { ShopDrawingRevision } from './entities/shop-drawing-revision.entity';
|
||||
|
||||
// Entities (Master Data - Contract Drawing)
|
||||
import { ContractDrawingVolume } from './entities/contract-drawing-volume.entity';
|
||||
import { ContractDrawingSubCategory } from './entities/contract-drawing-sub-category.entity';
|
||||
|
||||
// Entities (Master Data - Shop Drawing) - ✅ เพิ่มใหม่
|
||||
import { ShopDrawingMainCategory } from './entities/shop-drawing-main-category.entity';
|
||||
import { ShopDrawingSubCategory } from './entities/shop-drawing-sub-category.entity';
|
||||
|
||||
// Common Entities
|
||||
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
|
||||
|
||||
// Services
|
||||
import { ShopDrawingService } from './shop-drawing.service';
|
||||
import { ContractDrawingService } from './contract-drawing.service';
|
||||
import { DrawingMasterDataService } from './drawing-master-data.service'; // ✅ New
|
||||
|
||||
// Controllers
|
||||
import { ShopDrawingController } from './shop-drawing.controller';
|
||||
import { ContractDrawingController } from './contract-drawing.controller';
|
||||
import { DrawingMasterDataController } from './drawing-master-data.controller';
|
||||
// Modules
|
||||
import { FileStorageModule } from '../../common/file-storage/file-storage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
// Main
|
||||
ContractDrawing,
|
||||
ShopDrawing,
|
||||
ShopDrawingRevision,
|
||||
|
||||
// Master Data
|
||||
ContractDrawingVolume,
|
||||
ContractDrawingSubCategory,
|
||||
ShopDrawingMainCategory, // ✅
|
||||
ShopDrawingSubCategory, // ✅
|
||||
|
||||
// Common
|
||||
Attachment,
|
||||
]),
|
||||
FileStorageModule,
|
||||
],
|
||||
providers: [
|
||||
ShopDrawingService,
|
||||
ContractDrawingService,
|
||||
DrawingMasterDataService,
|
||||
],
|
||||
controllers: [
|
||||
ShopDrawingController,
|
||||
ContractDrawingController,
|
||||
DrawingMasterDataController,
|
||||
],
|
||||
exports: [ShopDrawingService, ContractDrawingService],
|
||||
})
|
||||
export class DrawingModule {}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
IsString,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateContractDrawingDto {
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number; // ✅ ใส่ !
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
contractDrawingNo!: string; // ✅ ใส่ !
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string; // ✅ ใส่ !
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
subCategoryId?: number; // ✅ ใส่ ?
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
volumeId?: number; // ✅ ใส่ ?
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@IsOptional()
|
||||
attachmentIds?: number[]; // ✅ ใส่ ?
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsDateString,
|
||||
IsArray,
|
||||
IsInt,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateShopDrawingRevisionDto {
|
||||
@IsString()
|
||||
revisionLabel!: string; // จำเป็น: ใส่ !
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
revisionDate?: string; // Optional: ใส่ ?
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string; // Optional: ใส่ ?
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@IsOptional()
|
||||
contractDrawingIds?: number[]; // Optional: ใส่ ?
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@IsOptional()
|
||||
attachmentIds?: number[]; // Optional: ใส่ ?
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
IsString,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsDateString,
|
||||
IsArray,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateShopDrawingDto {
|
||||
@IsInt()
|
||||
projectId!: number; // !
|
||||
|
||||
@IsString()
|
||||
drawingNumber!: string; // !
|
||||
|
||||
@IsString()
|
||||
title!: string; // !
|
||||
|
||||
@IsInt()
|
||||
mainCategoryId!: number; // !
|
||||
|
||||
@IsInt()
|
||||
subCategoryId!: number; // !
|
||||
|
||||
// First Revision Data (Optional ทั้งหมด เพราะถ้าไม่ส่งมาจะ Default ให้)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
revisionLabel?: string; // ?
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
revisionDate?: string; // ?
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string; // ?
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@IsOptional()
|
||||
contractDrawingIds?: number[]; // ?
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@IsOptional()
|
||||
attachmentIds?: number[]; // ?
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IsInt, IsOptional, IsString, IsNotEmpty } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class SearchContractDrawingDto {
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
@IsNotEmpty()
|
||||
projectId!: number; // จำเป็น: ใส่ !
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
volumeId?: number; // Optional: ใส่ ?
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
subCategoryId?: number; // Optional: ใส่ ?
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string; // Optional: ใส่ ?
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
page: number = 1; // มีค่า Default ไม่ต้องใส่ ! หรือ ?
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
pageSize: number = 20; // มีค่า Default ไม่ต้องใส่ ! หรือ ?
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { IsInt, IsOptional, IsString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class SearchShopDrawingDto {
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
projectId!: number; // จำเป็น: ใส่ !
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
mainCategoryId?: number; // Optional: ใส่ ?
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
subCategoryId?: number; // Optional: ใส่ ?
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
search?: string; // Optional: ใส่ ?
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
page: number = 1; // มีค่า Default
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
pageSize: number = 20; // มีค่า Default
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateContractDrawingDto } from './create-contract-drawing.dto';
|
||||
|
||||
export class UpdateContractDrawingDto extends PartialType(
|
||||
CreateContractDrawingDto,
|
||||
) {}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
|
||||
@Entity('contract_drawing_sub_cats')
|
||||
export class ContractDrawingSubCategory {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number; // เติม !
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
projectId!: number; // เติม !
|
||||
|
||||
@Column({ name: 'sub_cat_code', length: 50 })
|
||||
subCatCode!: string; // เติม !
|
||||
|
||||
@Column({ name: 'sub_cat_name', length: 255 })
|
||||
subCatName!: string; // เติม !
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string; // Nullable ใช้ ?
|
||||
|
||||
@Column({ name: 'sort_order', default: 0 })
|
||||
sortOrder!: number; // เติม !
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date; // เติม !
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date; // เติม !
|
||||
|
||||
@ManyToOne(() => Project)
|
||||
@JoinColumn({ name: 'project_id' })
|
||||
project!: Project; // เติม !
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
|
||||
@Entity('contract_drawing_volumes')
|
||||
export class ContractDrawingVolume {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number; // เติม !
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
projectId!: number; // เติม !
|
||||
|
||||
@Column({ name: 'volume_code', length: 50 })
|
||||
volumeCode!: string; // เติม !
|
||||
|
||||
@Column({ name: 'volume_name', length: 255 })
|
||||
volumeName!: string; // เติม !
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string; // Nullable ใช้ ?
|
||||
|
||||
@Column({ name: 'sort_order', default: 0 })
|
||||
sortOrder!: number; // เติม !
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date; // เติม !
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date; // เติม !
|
||||
|
||||
@ManyToOne(() => Project)
|
||||
@JoinColumn({ name: 'project_id' })
|
||||
project!: Project; // เติม ! (ตัวที่ Error)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from 'typeorm';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
import { User } from '../../user/entities/user.entity';
|
||||
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
||||
import { ContractDrawingSubCategory } from './contract-drawing-sub-category.entity';
|
||||
import { ContractDrawingVolume } from './contract-drawing-volume.entity';
|
||||
|
||||
@Entity('contract_drawings')
|
||||
export class ContractDrawing {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number; // ! ห้ามว่าง
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
projectId!: number; // ! ห้ามว่าง
|
||||
|
||||
@Column({ name: 'condwg_no', length: 255 })
|
||||
contractDrawingNo!: string; // ! ห้ามว่าง
|
||||
|
||||
@Column({ length: 255 })
|
||||
title!: string; // ! ห้ามว่าง
|
||||
|
||||
@Column({ name: 'sub_cat_id', nullable: true })
|
||||
subCategoryId?: number; // ? ว่างได้ (Nullable)
|
||||
|
||||
@Column({ name: 'volume_id', nullable: true })
|
||||
volumeId?: number; // ? ว่างได้ (Nullable)
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date; // ! ห้ามว่าง
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date; // ! ห้ามว่าง
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at' })
|
||||
deletedAt?: Date; // ? ว่างได้ (Nullable)
|
||||
|
||||
@Column({ name: 'updated_by', nullable: true })
|
||||
updatedBy?: number; // ? ว่างได้ (Nullable)
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@ManyToOne(() => Project)
|
||||
@JoinColumn({ name: 'project_id' })
|
||||
project!: Project; // ! ห้ามว่าง
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'updated_by' })
|
||||
updater?: User; // ? ว่างได้
|
||||
|
||||
@ManyToOne(() => ContractDrawingSubCategory)
|
||||
@JoinColumn({ name: 'sub_cat_id' })
|
||||
subCategory?: ContractDrawingSubCategory; // ? ว่างได้ (สัมพันธ์กับ subCategoryId)
|
||||
|
||||
@ManyToOne(() => ContractDrawingVolume)
|
||||
@JoinColumn({ name: 'volume_id' })
|
||||
volume?: ContractDrawingVolume; // ? แก้ไขตรงนี้: ใส่ ? เพราะ volumeId เป็น Nullable
|
||||
|
||||
@ManyToMany(() => Attachment)
|
||||
@JoinTable({
|
||||
name: 'contract_drawing_attachments',
|
||||
joinColumn: { name: 'contract_drawing_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'attachment_id', referencedColumnName: 'id' },
|
||||
})
|
||||
attachments!: Attachment[]; // ! ห้ามว่าง (TypeORM จะ return [] ถ้าไม่มี)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('shop_drawing_main_categories')
|
||||
export class ShopDrawingMainCategory {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number; // เติม !
|
||||
|
||||
@Column({ name: 'main_category_code', length: 50, unique: true })
|
||||
mainCategoryCode!: string; // เติม !
|
||||
|
||||
@Column({ name: 'main_category_name', length: 255 })
|
||||
mainCategoryName!: string; // เติม !
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string; // nullable
|
||||
|
||||
@Column({ name: 'sort_order', default: 0 })
|
||||
sortOrder!: number; // เติม !
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive!: boolean; // เติม !
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date; // เติม !
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date; // เติม !
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from 'typeorm';
|
||||
import { ShopDrawing } from './shop-drawing.entity';
|
||||
import { ContractDrawing } from './contract-drawing.entity';
|
||||
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
||||
|
||||
@Entity('shop_drawing_revisions')
|
||||
export class ShopDrawingRevision {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number; // เติม !
|
||||
|
||||
@Column({ name: 'shop_drawing_id' })
|
||||
shopDrawingId!: number; // เติม !
|
||||
|
||||
@Column({ name: 'revision_number' })
|
||||
revisionNumber!: number; // เติม !
|
||||
|
||||
@Column({ name: 'revision_label', length: 10, nullable: true })
|
||||
revisionLabel?: string; // nullable ใช้ ?
|
||||
|
||||
@Column({ name: 'revision_date', type: 'date', nullable: true })
|
||||
revisionDate?: Date; // nullable ใช้ ?
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string; // nullable ใช้ ?
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date; // เติม !
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => ShopDrawing, (shopDrawing) => shopDrawing.revisions, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'shop_drawing_id' })
|
||||
shopDrawing!: ShopDrawing; // เติม !
|
||||
|
||||
// References to Contract Drawings (M:N)
|
||||
@ManyToMany(() => ContractDrawing)
|
||||
@JoinTable({
|
||||
name: 'shop_drawing_revision_contract_refs',
|
||||
joinColumn: {
|
||||
name: 'shop_drawing_revision_id',
|
||||
referencedColumnName: 'id',
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: 'contract_drawing_id',
|
||||
referencedColumnName: 'id',
|
||||
},
|
||||
})
|
||||
contractDrawings!: ContractDrawing[]; // เติม !
|
||||
|
||||
// Attachments (M:N)
|
||||
@ManyToMany(() => Attachment)
|
||||
@JoinTable({
|
||||
name: 'shop_drawing_revision_attachments',
|
||||
joinColumn: {
|
||||
name: 'shop_drawing_revision_id',
|
||||
referencedColumnName: 'id',
|
||||
},
|
||||
inverseJoinColumn: { name: 'attachment_id', referencedColumnName: 'id' },
|
||||
})
|
||||
attachments!: Attachment[]; // เติม ! (ตัวที่ error)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
|
||||
|
||||
@Entity('shop_drawing_sub_categories')
|
||||
export class ShopDrawingSubCategory {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number; // เติม ! (ตัวที่ error)
|
||||
|
||||
@Column({ name: 'sub_category_code', length: 50, unique: true })
|
||||
subCategoryCode!: string; // เติม !
|
||||
|
||||
@Column({ name: 'sub_category_name', length: 255 })
|
||||
subCategoryName!: string; // เติม !
|
||||
|
||||
@Column({ name: 'main_category_id' })
|
||||
mainCategoryId!: number; // เติม !
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string; // nullable ใช้ ?
|
||||
|
||||
@Column({ name: 'sort_order', default: 0 })
|
||||
sortOrder!: number; // เติม !
|
||||
|
||||
@Column({ name: 'is_active', default: true })
|
||||
isActive!: boolean; // เติม !
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date; // เติม !
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date; // เติม !
|
||||
|
||||
// Relation to Main Category
|
||||
@ManyToOne(() => ShopDrawingMainCategory)
|
||||
@JoinColumn({ name: 'main_category_id' })
|
||||
mainCategory!: ShopDrawingMainCategory; // เติม !
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ShopDrawingRevision } from './shop-drawing-revision.entity';
|
||||
import { Project } from '../../project/entities/project.entity';
|
||||
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
|
||||
import { ShopDrawingSubCategory } from './shop-drawing-sub-category.entity';
|
||||
|
||||
@Entity('shop_drawings')
|
||||
export class ShopDrawing {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number; // เติม !
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
projectId!: number; // เติม !
|
||||
|
||||
@Column({ name: 'drawing_number', length: 100, unique: true })
|
||||
drawingNumber!: string; // เติม !
|
||||
|
||||
@Column({ length: 500 })
|
||||
title!: string; // เติม !
|
||||
|
||||
@Column({ name: 'main_category_id' })
|
||||
mainCategoryId!: number; // เติม !
|
||||
|
||||
@Column({ name: 'sub_category_id' })
|
||||
subCategoryId!: number; // เติม !
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date; // เติม !
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt!: Date; // เติม !
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at' })
|
||||
deletedAt?: Date; // nullable
|
||||
|
||||
@Column({ name: 'updated_by', nullable: true })
|
||||
updatedBy?: number; // nullable
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@ManyToOne(() => Project)
|
||||
@JoinColumn({ name: 'project_id' })
|
||||
project!: Project; // เติม ! (ตัวที่ error)
|
||||
|
||||
@ManyToOne(() => ShopDrawingMainCategory)
|
||||
@JoinColumn({ name: 'main_category_id' })
|
||||
mainCategory!: ShopDrawingMainCategory; // เติม !
|
||||
|
||||
@ManyToOne(() => ShopDrawingSubCategory)
|
||||
@JoinColumn({ name: 'sub_category_id' })
|
||||
subCategory!: ShopDrawingSubCategory; // เติม !
|
||||
|
||||
@OneToMany(() => ShopDrawingRevision, (revision) => revision.shopDrawing, {
|
||||
cascade: true,
|
||||
})
|
||||
revisions!: ShopDrawingRevision[]; // เติม !
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { ShopDrawingService } from './shop-drawing.service';
|
||||
import { CreateShopDrawingDto } from './dto/create-shop-drawing.dto';
|
||||
import { SearchShopDrawingDto } from './dto/search-shop-drawing.dto';
|
||||
import { CreateShopDrawingRevisionDto } from './dto/create-shop-drawing-revision.dto';
|
||||
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
@ApiTags('Shop Drawings')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
@Controller('drawings/shop')
|
||||
export class ShopDrawingController {
|
||||
constructor(private readonly shopDrawingService: ShopDrawingService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create new Shop Drawing with initial revision' })
|
||||
@RequirePermission('drawing.create') // อ้างอิง Permission จาก Seed
|
||||
create(@Body() createDto: CreateShopDrawingDto, @CurrentUser() user: User) {
|
||||
return this.shopDrawingService.create(createDto, user);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Search Shop Drawings' })
|
||||
@RequirePermission('drawing.view')
|
||||
findAll(@Query() searchDto: SearchShopDrawingDto) {
|
||||
return this.shopDrawingService.findAll(searchDto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Shop Drawing details with revisions' })
|
||||
@RequirePermission('drawing.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.shopDrawingService.findOne(id);
|
||||
}
|
||||
|
||||
@Post(':id/revisions')
|
||||
@ApiOperation({ summary: 'Add new revision to existing Shop Drawing' })
|
||||
@RequirePermission('drawing.create') // หรือ drawing.edit ตาม Logic องค์กร
|
||||
createRevision(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() createRevisionDto: CreateShopDrawingRevisionDto,
|
||||
) {
|
||||
return this.shopDrawingService.createRevision(id, createRevisionDto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
ConflictException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, In, Brackets } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { ShopDrawing } from './entities/shop-drawing.entity';
|
||||
import { ShopDrawingRevision } from './entities/shop-drawing-revision.entity';
|
||||
import { ContractDrawing } from './entities/contract-drawing.entity';
|
||||
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
// DTOs
|
||||
import { CreateShopDrawingDto } from './dto/create-shop-drawing.dto';
|
||||
import { CreateShopDrawingRevisionDto } from './dto/create-shop-drawing-revision.dto';
|
||||
import { SearchShopDrawingDto } from './dto/search-shop-drawing.dto';
|
||||
|
||||
// Services
|
||||
import { FileStorageService } from '../../common/file-storage/file-storage.service';
|
||||
|
||||
@Injectable()
|
||||
export class ShopDrawingService {
|
||||
private readonly logger = new Logger(ShopDrawingService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ShopDrawing)
|
||||
private shopDrawingRepo: Repository<ShopDrawing>,
|
||||
@InjectRepository(ShopDrawingRevision)
|
||||
private revisionRepo: Repository<ShopDrawingRevision>,
|
||||
@InjectRepository(ContractDrawing)
|
||||
private contractDrawingRepo: Repository<ContractDrawing>,
|
||||
@InjectRepository(Attachment)
|
||||
private attachmentRepo: Repository<Attachment>,
|
||||
private fileStorageService: FileStorageService,
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* สร้าง Shop Drawing ใหม่ พร้อม Revision แรก (Rev 0)
|
||||
* ทำงานภายใต้ Database Transaction เดียวกัน
|
||||
*/
|
||||
async create(createDto: CreateShopDrawingDto, user: User) {
|
||||
// 1. ตรวจสอบเลขที่แบบซ้ำ (Unique Check)
|
||||
const exists = await this.shopDrawingRepo.findOne({
|
||||
where: { drawingNumber: createDto.drawingNumber },
|
||||
});
|
||||
if (exists) {
|
||||
throw new ConflictException(
|
||||
`Drawing number "${createDto.drawingNumber}" already exists.`,
|
||||
);
|
||||
}
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 2. เตรียมข้อมูล Relations (Contract Drawings & Attachments)
|
||||
let contractDrawings: ContractDrawing[] = [];
|
||||
if (createDto.contractDrawingIds?.length) {
|
||||
contractDrawings = await this.contractDrawingRepo.findBy({
|
||||
id: In(createDto.contractDrawingIds),
|
||||
});
|
||||
}
|
||||
|
||||
let attachments: Attachment[] = [];
|
||||
if (createDto.attachmentIds?.length) {
|
||||
attachments = await this.attachmentRepo.findBy({
|
||||
id: In(createDto.attachmentIds),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. สร้าง Master Shop Drawing
|
||||
const shopDrawing = queryRunner.manager.create(ShopDrawing, {
|
||||
projectId: createDto.projectId,
|
||||
drawingNumber: createDto.drawingNumber,
|
||||
title: createDto.title,
|
||||
mainCategoryId: createDto.mainCategoryId,
|
||||
subCategoryId: createDto.subCategoryId,
|
||||
updatedBy: user.user_id,
|
||||
});
|
||||
const savedShopDrawing = await queryRunner.manager.save(shopDrawing);
|
||||
|
||||
// 4. สร้าง First Revision (Rev 0)
|
||||
const revision = queryRunner.manager.create(ShopDrawingRevision, {
|
||||
shopDrawingId: savedShopDrawing.id,
|
||||
revisionNumber: 0, // เริ่มต้นที่ 0 เสมอ
|
||||
revisionLabel: createDto.revisionLabel || '0',
|
||||
revisionDate: createDto.revisionDate
|
||||
? new Date(createDto.revisionDate)
|
||||
: new Date(),
|
||||
description: createDto.description,
|
||||
contractDrawings: contractDrawings, // ผูก M:N Relation
|
||||
attachments: attachments, // ผูก M:N Relation
|
||||
});
|
||||
await queryRunner.manager.save(revision);
|
||||
|
||||
// 5. Commit Files (ย้ายไฟล์จาก Temp -> Permanent)
|
||||
if (createDto.attachmentIds?.length) {
|
||||
// ✅ FIX: ใช้ commitFiles และแปลง number[] -> string[]
|
||||
await this.fileStorageService.commit(
|
||||
createDto.attachmentIds.map(String),
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
...savedShopDrawing,
|
||||
currentRevision: revision,
|
||||
};
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
// ✅ FIX: Cast err เป็น Error
|
||||
this.logger.error(
|
||||
`Failed to create shop drawing: ${(err as Error).message}`,
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* เพิ่ม Revision ใหม่ให้กับ Shop Drawing เดิม (Add Revision)
|
||||
* เช่น Rev 0 -> Rev A
|
||||
*/
|
||||
async createRevision(
|
||||
shopDrawingId: number,
|
||||
createDto: CreateShopDrawingRevisionDto,
|
||||
) {
|
||||
// 1. ตรวจสอบว่ามี Master Drawing อยู่จริง
|
||||
const shopDrawing = await this.shopDrawingRepo.findOneBy({
|
||||
id: shopDrawingId,
|
||||
});
|
||||
if (!shopDrawing) {
|
||||
throw new NotFoundException('Shop Drawing not found');
|
||||
}
|
||||
|
||||
// 2. ตรวจสอบ Label ซ้ำใน Drawing เดียวกัน
|
||||
const exists = await this.revisionRepo.findOne({
|
||||
where: { shopDrawingId, revisionLabel: createDto.revisionLabel },
|
||||
});
|
||||
if (exists) {
|
||||
throw new ConflictException(
|
||||
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`,
|
||||
);
|
||||
}
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 3. เตรียม Relations
|
||||
let contractDrawings: ContractDrawing[] = [];
|
||||
if (createDto.contractDrawingIds?.length) {
|
||||
contractDrawings = await this.contractDrawingRepo.findBy({
|
||||
id: In(createDto.contractDrawingIds),
|
||||
});
|
||||
}
|
||||
|
||||
let attachments: Attachment[] = [];
|
||||
if (createDto.attachmentIds?.length) {
|
||||
attachments = await this.attachmentRepo.findBy({
|
||||
id: In(createDto.attachmentIds),
|
||||
});
|
||||
}
|
||||
|
||||
// 4. หา Revision Number ล่าสุดเพื่อ +1 (Running Number ภายใน)
|
||||
const latestRev = await this.revisionRepo.findOne({
|
||||
where: { shopDrawingId },
|
||||
order: { revisionNumber: 'DESC' },
|
||||
});
|
||||
const nextRevNum = (latestRev?.revisionNumber ?? -1) + 1;
|
||||
|
||||
// 5. บันทึก Revision ใหม่
|
||||
const revision = queryRunner.manager.create(ShopDrawingRevision, {
|
||||
shopDrawingId,
|
||||
revisionNumber: nextRevNum,
|
||||
revisionLabel: createDto.revisionLabel,
|
||||
revisionDate: createDto.revisionDate
|
||||
? new Date(createDto.revisionDate)
|
||||
: new Date(),
|
||||
description: createDto.description,
|
||||
contractDrawings: contractDrawings,
|
||||
attachments: attachments,
|
||||
});
|
||||
await queryRunner.manager.save(revision);
|
||||
|
||||
// 6. Commit Files
|
||||
if (createDto.attachmentIds?.length) {
|
||||
// ✅ FIX: ใช้ commitFiles และแปลง number[] -> string[]
|
||||
await this.fileStorageService.commit(
|
||||
createDto.attachmentIds.map(String),
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return revision;
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
// ✅ FIX: Cast err เป็น Error
|
||||
this.logger.error(`Failed to create revision: ${(err as Error).message}`);
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ค้นหา Shop Drawing (Search & Filter)
|
||||
* รองรับการค้นหาด้วย Text และกรองตาม Category
|
||||
*/
|
||||
async findAll(searchDto: SearchShopDrawingDto) {
|
||||
const {
|
||||
projectId,
|
||||
mainCategoryId,
|
||||
subCategoryId,
|
||||
search,
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
} = searchDto;
|
||||
|
||||
const query = this.shopDrawingRepo
|
||||
.createQueryBuilder('sd')
|
||||
.leftJoinAndSelect('sd.mainCategory', 'mainCat')
|
||||
.leftJoinAndSelect('sd.subCategory', 'subCat')
|
||||
.leftJoinAndSelect('sd.revisions', 'rev')
|
||||
.where('sd.projectId = :projectId', { projectId });
|
||||
|
||||
if (mainCategoryId) {
|
||||
query.andWhere('sd.mainCategoryId = :mainCategoryId', { mainCategoryId });
|
||||
}
|
||||
|
||||
if (subCategoryId) {
|
||||
query.andWhere('sd.subCategoryId = :subCategoryId', { subCategoryId });
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where('sd.drawingNumber LIKE :search', {
|
||||
search: `%${search}%`,
|
||||
}).orWhere('sd.title LIKE :search', { search: `%${search}%` });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
query.orderBy('sd.updatedAt', 'DESC');
|
||||
|
||||
const skip = (page - 1) * pageSize;
|
||||
query.skip(skip).take(pageSize);
|
||||
|
||||
const [items, total] = await query.getManyAndCount();
|
||||
|
||||
// Transform Data: เลือก Revision ล่าสุดมาแสดงเป็น currentRevision
|
||||
const transformedItems = items.map((item) => {
|
||||
item.revisions.sort((a, b) => b.revisionNumber - a.revisionNumber);
|
||||
const currentRevision = item.revisions[0];
|
||||
return {
|
||||
...item,
|
||||
currentRevision,
|
||||
revisions: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: transformedItems,
|
||||
meta: {
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ดูรายละเอียด Shop Drawing (Get One)
|
||||
*/
|
||||
async findOne(id: number) {
|
||||
const shopDrawing = await this.shopDrawingRepo.findOne({
|
||||
where: { id },
|
||||
relations: [
|
||||
'mainCategory',
|
||||
'subCategory',
|
||||
'revisions',
|
||||
'revisions.attachments',
|
||||
'revisions.contractDrawings',
|
||||
],
|
||||
order: {
|
||||
revisions: { revisionNumber: 'DESC' },
|
||||
},
|
||||
});
|
||||
|
||||
if (!shopDrawing) {
|
||||
throw new NotFoundException(`Shop Drawing ID ${id} not found`);
|
||||
}
|
||||
|
||||
return shopDrawing;
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบ Shop Drawing (Soft Delete)
|
||||
*/
|
||||
async remove(id: number, user: User) {
|
||||
const shopDrawing = await this.findOne(id);
|
||||
|
||||
shopDrawing.updatedBy = user.user_id;
|
||||
await this.shopDrawingRepo.save(shopDrawing);
|
||||
|
||||
return this.shopDrawingRepo.softRemove(shopDrawing);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user