251225:1703 On going update to 1.7.0: Refoctory drawing Module not finish
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-25 17:03:33 +07:00
parent 7db6a003db
commit cd73cc1549
60 changed files with 8201 additions and 832 deletions

View File

@@ -90,8 +90,8 @@ export class DocumentNumberingController {
async previewNumber(@Body() dto: PreviewNumberDto) {
return this.numberingService.previewNumber({
projectId: dto.projectId,
originatorOrganizationId: dto.originatorId,
typeId: dto.typeId,
originatorOrganizationId: dto.originatorOrganizationId,
typeId: dto.correspondenceTypeId,
subTypeId: dto.subTypeId,
rfaTypeId: dto.rfaTypeId,
disciplineId: dto.disciplineId,

View File

@@ -12,12 +12,12 @@ export class PreviewNumberDto {
@ApiProperty({ description: 'Originator organization ID' })
@IsInt()
@Type(() => Number)
originatorId!: number;
originatorOrganizationId!: number;
@ApiProperty({ description: 'Correspondence type ID' })
@IsInt()
@Type(() => Number)
typeId!: number;
correspondenceTypeId!: number;
@ApiPropertyOptional({ description: 'Sub type ID (for TRANSMITTAL)' })
@IsOptional()

View File

@@ -108,8 +108,8 @@ export class FormatService {
'{ORG}': orgCode,
'{RECIPIENT}': recipientCode,
'{DISCIPLINE}': disciplineCode,
'{YEAR}': year.toString().substring(2),
'{YEAR:BE}': (year + 543).toString().substring(2),
'{YEAR}': year.toString(),
'{YEAR:BE}': (year + 543).toString(),
'{REV}': '0',
};
}

View File

@@ -0,0 +1,101 @@
import {
Controller,
Get,
Post,
Delete,
Body,
Param,
Query,
ParseIntPipe,
HttpCode,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
// Guards
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
// Services
import { AsBuiltDrawingService } from './asbuilt-drawing.service';
// DTOs
import { CreateAsBuiltDrawingDto } from './dto/create-asbuilt-drawing.dto';
import { CreateAsBuiltDrawingRevisionDto } from './dto/create-asbuilt-drawing-revision.dto';
import { SearchAsBuiltDrawingDto } from './dto/search-asbuilt-drawing.dto';
// Decorators
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { Audit } from '../../common/decorators/audit.decorator';
import { User } from '../user/entities/user.entity';
@ApiTags('Drawings - AS Built')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('drawings/asbuilt')
export class AsBuiltDrawingController {
constructor(private readonly asBuiltDrawingService: AsBuiltDrawingService) {}
@Post()
@ApiOperation({ summary: 'Create new AS Built Drawing' })
@ApiResponse({ status: 201, description: 'AS Built Drawing created' })
@ApiResponse({ status: 409, description: 'Drawing number already exists' })
@RequirePermission('drawing.create')
@Audit('drawing.create', 'asbuilt_drawing')
async create(
@Body() createDto: CreateAsBuiltDrawingDto,
@CurrentUser() user: User
) {
return this.asBuiltDrawingService.create(createDto, user);
}
@Post(':id/revisions')
@ApiOperation({ summary: 'Create new revision for AS Built Drawing' })
@ApiResponse({ status: 201, description: 'Revision created' })
@ApiResponse({ status: 404, description: 'AS Built Drawing not found' })
@ApiResponse({ status: 409, description: 'Revision label already exists' })
async createRevision(
@Param('id', ParseIntPipe) id: number,
@Body() createDto: CreateAsBuiltDrawingRevisionDto
) {
return this.asBuiltDrawingService.createRevision(id, createDto);
}
@Get()
@ApiOperation({ summary: 'List AS Built Drawings with search/pagination' })
@ApiResponse({ status: 200, description: 'List of AS Built Drawings' })
@RequirePermission('drawing.view')
async findAll(@Query() searchDto: SearchAsBuiltDrawingDto) {
return this.asBuiltDrawingService.findAll(searchDto);
}
@Get(':id')
@ApiOperation({ summary: 'Get AS Built Drawing by ID' })
@ApiResponse({ status: 200, description: 'AS Built Drawing details' })
@ApiResponse({ status: 404, description: 'AS Built Drawing not found' })
@RequirePermission('drawing.view')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.asBuiltDrawingService.findOne(id);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Soft delete AS Built Drawing' })
@ApiResponse({ status: 204, description: 'AS Built Drawing deleted' })
@ApiResponse({ status: 404, description: 'AS Built Drawing not found' })
@RequirePermission('drawing.delete')
@Audit('drawing.delete', 'asbuilt_drawing')
async remove(
@Param('id', ParseIntPipe) id: number,
@CurrentUser() user: User
) {
return this.asBuiltDrawingService.remove(id, user);
}
}

View File

@@ -0,0 +1,307 @@
import {
Injectable,
NotFoundException,
ConflictException,
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In } from 'typeorm';
// Entities
import { AsBuiltDrawing } from './entities/asbuilt-drawing.entity';
import { AsBuiltDrawingRevision } from './entities/asbuilt-drawing-revision.entity';
import { ShopDrawingRevision } from './entities/shop-drawing-revision.entity';
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
import { User } from '../user/entities/user.entity';
// DTOs
import { CreateAsBuiltDrawingDto } from './dto/create-asbuilt-drawing.dto';
import { CreateAsBuiltDrawingRevisionDto } from './dto/create-asbuilt-drawing-revision.dto';
import { SearchAsBuiltDrawingDto } from './dto/search-asbuilt-drawing.dto';
// Services
import { FileStorageService } from '../../common/file-storage/file-storage.service';
@Injectable()
export class AsBuiltDrawingService {
private readonly logger = new Logger(AsBuiltDrawingService.name);
constructor(
@InjectRepository(AsBuiltDrawing)
private asBuiltDrawingRepo: Repository<AsBuiltDrawing>,
@InjectRepository(AsBuiltDrawingRevision)
private revisionRepo: Repository<AsBuiltDrawingRevision>,
@InjectRepository(ShopDrawingRevision)
private shopDrawingRevisionRepo: Repository<ShopDrawingRevision>,
@InjectRepository(Attachment)
private attachmentRepo: Repository<Attachment>,
private fileStorageService: FileStorageService,
private dataSource: DataSource
) {}
/**
* สร้าง AS Built Drawing ใหม่ พร้อม Revision แรก (Rev 0)
*/
async create(createDto: CreateAsBuiltDrawingDto, user: User) {
// 1. Check Duplicate
const exists = await this.asBuiltDrawingRepo.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. Prepare Relations
let shopDrawingRevisions: ShopDrawingRevision[] = [];
if (createDto.shopDrawingRevisionIds?.length) {
shopDrawingRevisions = await this.shopDrawingRevisionRepo.findBy({
id: In(createDto.shopDrawingRevisionIds),
});
}
let attachments: Attachment[] = [];
if (createDto.attachmentIds?.length) {
attachments = await this.attachmentRepo.findBy({
id: In(createDto.attachmentIds),
});
}
// 3. Create Master AS Built Drawing
const asBuiltDrawing = queryRunner.manager.create(AsBuiltDrawing, {
projectId: createDto.projectId,
drawingNumber: createDto.drawingNumber,
mainCategoryId: createDto.mainCategoryId,
subCategoryId: createDto.subCategoryId,
updatedBy: user.user_id,
});
const savedDrawing = await queryRunner.manager.save(asBuiltDrawing);
// 4. Create First Revision (Rev 0)
const revision = queryRunner.manager.create(AsBuiltDrawingRevision, {
asBuiltDrawingId: savedDrawing.id,
revisionNumber: 0,
revisionLabel: createDto.revisionLabel || '0',
title: createDto.title,
legacyDrawingNumber: createDto.legacyDrawingNumber,
revisionDate: createDto.revisionDate
? new Date(createDto.revisionDate)
: new Date(),
description: createDto.description,
shopDrawingRevisions: shopDrawingRevisions,
attachments: attachments,
});
await queryRunner.manager.save(revision);
// 5. Commit Files
if (createDto.attachmentIds?.length) {
await this.fileStorageService.commit(
createDto.attachmentIds.map(String)
);
}
await queryRunner.commitTransaction();
return {
...savedDrawing,
currentRevision: revision,
};
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Failed to create AS Built drawing: ${(err as Error).message}`
);
throw err;
} finally {
await queryRunner.release();
}
}
/**
* เพิ่ม Revision ใหม่ (Add Revision)
*/
async createRevision(
asBuiltDrawingId: number,
createDto: CreateAsBuiltDrawingRevisionDto
) {
const asBuiltDrawing = await this.asBuiltDrawingRepo.findOneBy({
id: asBuiltDrawingId,
});
if (!asBuiltDrawing) {
throw new NotFoundException('AS Built Drawing not found');
}
const exists = await this.revisionRepo.findOne({
where: { asBuiltDrawingId, 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 {
let shopDrawingRevisions: ShopDrawingRevision[] = [];
if (createDto.shopDrawingRevisionIds?.length) {
shopDrawingRevisions = await this.shopDrawingRevisionRepo.findBy({
id: In(createDto.shopDrawingRevisionIds),
});
}
let attachments: Attachment[] = [];
if (createDto.attachmentIds?.length) {
attachments = await this.attachmentRepo.findBy({
id: In(createDto.attachmentIds),
});
}
const latestRev = await this.revisionRepo.findOne({
where: { asBuiltDrawingId },
order: { revisionNumber: 'DESC' },
});
const nextRevNum = (latestRev?.revisionNumber ?? -1) + 1;
const revision = queryRunner.manager.create(AsBuiltDrawingRevision, {
asBuiltDrawingId,
revisionNumber: nextRevNum,
revisionLabel: createDto.revisionLabel,
title: createDto.title,
legacyDrawingNumber: createDto.legacyDrawingNumber,
revisionDate: createDto.revisionDate
? new Date(createDto.revisionDate)
: new Date(),
description: createDto.description,
shopDrawingRevisions: shopDrawingRevisions,
attachments: attachments,
});
await queryRunner.manager.save(revision);
if (createDto.attachmentIds?.length) {
await this.fileStorageService.commit(
createDto.attachmentIds.map(String)
);
}
await queryRunner.commitTransaction();
return revision;
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(`Failed to create revision: ${(err as Error).message}`);
throw err;
} finally {
await queryRunner.release();
}
}
/**
* ค้นหา AS Built Drawing
*/
async findAll(searchDto: SearchAsBuiltDrawingDto) {
const {
projectId,
mainCategoryId,
subCategoryId,
search,
page = 1,
limit = 20,
} = searchDto;
const query = this.asBuiltDrawingRepo
.createQueryBuilder('abd')
.leftJoinAndSelect('abd.mainCategory', 'mainCat')
.leftJoinAndSelect('abd.subCategory', 'subCat')
.leftJoinAndSelect('abd.revisions', 'rev')
.where('abd.projectId = :projectId', { projectId });
if (mainCategoryId) {
query.andWhere('abd.mainCategoryId = :mainCategoryId', {
mainCategoryId,
});
}
if (subCategoryId) {
query.andWhere('abd.subCategoryId = :subCategoryId', { subCategoryId });
}
if (search) {
query.andWhere('abd.drawingNumber LIKE :search', {
search: `%${search}%`,
});
}
query.orderBy('abd.updatedAt', 'DESC');
const skip = (page - 1) * limit;
query.skip(skip).take(limit);
const [items, total] = await query.getManyAndCount();
// Transform Data
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,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
/**
* ดูรายละเอียด AS Built Drawing
*/
async findOne(id: number) {
const asBuiltDrawing = await this.asBuiltDrawingRepo.findOne({
where: { id },
relations: [
'mainCategory',
'subCategory',
'revisions',
'revisions.attachments',
'revisions.shopDrawingRevisions',
],
order: {
revisions: { revisionNumber: 'DESC' },
},
});
if (!asBuiltDrawing) {
throw new NotFoundException(`AS Built Drawing ID ${id} not found`);
}
return asBuiltDrawing;
}
/**
* ลบ AS Built Drawing
*/
async remove(id: number, user: User) {
const asBuiltDrawing = await this.findOne(id);
asBuiltDrawing.updatedBy = user.user_id;
await this.asBuiltDrawingRepo.save(asBuiltDrawing);
return this.asBuiltDrawingRepo.softRemove(asBuiltDrawing);
}
}

View File

@@ -2,12 +2,20 @@ import {
Controller,
Get,
Post,
Patch,
Delete,
Body,
Param,
Query,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiBearerAuth,
ApiQuery,
} from '@nestjs/swagger';
import { DrawingMasterDataService } from './drawing-master-data.service';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
@@ -17,55 +25,310 @@ import { RequirePermission } from '../../common/decorators/require-permission.de
@ApiTags('Drawing Master Data')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('drawings/master')
@Controller('drawings/master-data')
export class DrawingMasterDataController {
// ✅ ต้องมี export ตรงนี้
constructor(private readonly masterDataService: DrawingMasterDataService) {}
// --- Contract Drawing Endpoints ---
// =====================================================
// Contract Drawing Volumes
// =====================================================
@Get('contract/volumes')
@ApiOperation({ summary: 'List Contract Drawing Volumes' })
@ApiQuery({ name: 'projectId', required: true, type: Number })
@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 จริงในการผลิต
@ApiOperation({ summary: 'Create Volume' })
@RequirePermission('master_data.drawing_category.manage')
createVolume(
@Body()
body: {
projectId: number;
volumeCode: string;
volumeName: string;
description?: string;
sortOrder: number;
}
) {
return this.masterDataService.createVolume(body);
}
@Patch('contract/volumes/:id')
@ApiOperation({ summary: 'Update Volume' })
@RequirePermission('master_data.drawing_category.manage')
updateVolume(
@Param('id', ParseIntPipe) id: number,
@Body()
body: {
volumeCode?: string;
volumeName?: string;
description?: string;
sortOrder?: number;
}
) {
return this.masterDataService.updateVolume(id, body);
}
@Delete('contract/volumes/:id')
@ApiOperation({ summary: 'Delete Volume' })
@RequirePermission('master_data.drawing_category.manage')
deleteVolume(@Param('id', ParseIntPipe) id: number) {
return this.masterDataService.deleteVolume(id);
}
// =====================================================
// Contract Drawing Categories
// =====================================================
@Get('contract/categories')
@ApiOperation({ summary: 'List Contract Drawing Categories' })
@ApiQuery({ name: 'projectId', required: true, type: Number })
@RequirePermission('document.view')
getCategories(@Query('projectId', ParseIntPipe) projectId: number) {
return this.masterDataService.findAllCategories(projectId);
}
@Post('contract/categories')
@ApiOperation({ summary: 'Create Category' })
@RequirePermission('master_data.drawing_category.manage')
createCategory(
@Body()
body: {
projectId: number;
catCode: string;
catName: string;
description?: string;
sortOrder: number;
}
) {
return this.masterDataService.createCategory(body);
}
@Patch('contract/categories/:id')
@ApiOperation({ summary: 'Update Category' })
@RequirePermission('master_data.drawing_category.manage')
updateCategory(
@Param('id', ParseIntPipe) id: number,
@Body()
body: {
catCode?: string;
catName?: string;
description?: string;
sortOrder?: number;
}
) {
return this.masterDataService.updateCategory(id, body);
}
@Delete('contract/categories/:id')
@ApiOperation({ summary: 'Delete Category' })
@RequirePermission('master_data.drawing_category.manage')
deleteCategory(@Param('id', ParseIntPipe) id: number) {
return this.masterDataService.deleteCategory(id);
}
// =====================================================
// Contract Drawing Sub-Categories
// =====================================================
@Get('contract/sub-categories')
@ApiOperation({ summary: 'List Contract Drawing Sub-Categories' })
@ApiQuery({ name: 'projectId', required: true, type: Number })
@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)' })
@ApiOperation({ summary: 'Create Contract Sub-Category' })
@RequirePermission('master_data.drawing_category.manage')
createContractSubCat(@Body() body: any) {
createContractSubCat(
@Body()
body: {
projectId: number;
subCatCode: string;
subCatName: string;
description?: string;
sortOrder: number;
}
) {
return this.masterDataService.createContractSubCat(body);
}
// --- Shop Drawing Endpoints ---
@Patch('contract/sub-categories/:id')
@ApiOperation({ summary: 'Update Contract Sub-Category' })
@RequirePermission('master_data.drawing_category.manage')
updateContractSubCat(
@Param('id', ParseIntPipe) id: number,
@Body()
body: {
subCatCode?: string;
subCatName?: string;
description?: string;
sortOrder?: number;
}
) {
return this.masterDataService.updateContractSubCat(id, body);
}
async deleteContractSubCat(@Param('id', ParseIntPipe) id: number) {
return this.masterDataService.deleteContractSubCat(id);
}
// =====================================================
// Contract Drawing Mappings
// =====================================================
@Get('contract/mappings')
@ApiOperation({ summary: 'List Contract Drawing Mappings' })
@ApiQuery({ name: 'projectId', required: true, type: Number })
@ApiQuery({ name: 'categoryId', required: false, type: Number })
@RequirePermission('document.view')
getContractMappings(
@Query('projectId', ParseIntPipe) projectId: number,
@Query('categoryId') categoryId?: number
) {
return this.masterDataService.findContractMappings(
projectId,
categoryId ? Number(categoryId) : undefined
);
}
@Post('contract/mappings')
@ApiOperation({ summary: 'Create Contract Drawing Mapping' })
@RequirePermission('master_data.drawing_category.manage')
createContractMapping(
@Body()
body: {
projectId: number;
categoryId: number;
subCategoryId: number;
}
) {
return this.masterDataService.createContractMapping(body);
}
@Delete('contract/mappings/:id')
@ApiOperation({ summary: 'Delete Contract Drawing Mapping' })
@RequirePermission('master_data.drawing_category.manage')
deleteContractMapping(@Param('id', ParseIntPipe) id: number) {
return this.masterDataService.deleteContractMapping(id);
}
// =====================================================
// Shop Drawing Main Categories
// =====================================================
@Get('shop/main-categories')
@ApiOperation({ summary: 'List Shop Drawing Main Categories' })
@ApiQuery({ name: 'projectId', required: true, type: Number })
@RequirePermission('document.view')
getShopMainCats() {
return this.masterDataService.findAllShopMainCats();
getShopMainCats(@Query('projectId', ParseIntPipe) projectId: number) {
return this.masterDataService.findAllShopMainCats(projectId);
}
@Post('shop/main-categories')
@ApiOperation({ summary: 'Create Shop Main Category' })
@RequirePermission('master_data.drawing_category.manage')
createShopMainCat(
@Body()
body: {
projectId: number;
mainCategoryCode: string;
mainCategoryName: string;
description?: string;
isActive?: boolean;
sortOrder: number;
}
) {
return this.masterDataService.createShopMainCat(body);
}
@Patch('shop/main-categories/:id')
@ApiOperation({ summary: 'Update Shop Main Category' })
@RequirePermission('master_data.drawing_category.manage')
updateShopMainCat(
@Param('id', ParseIntPipe) id: number,
@Body()
body: {
mainCategoryCode?: string;
mainCategoryName?: string;
description?: string;
isActive?: boolean;
sortOrder?: number;
}
) {
return this.masterDataService.updateShopMainCat(id, body);
}
@Delete('shop/main-categories/:id')
@ApiOperation({ summary: 'Delete Shop Main Category' })
@RequirePermission('master_data.drawing_category.manage')
deleteShopMainCat(@Param('id', ParseIntPipe) id: number) {
return this.masterDataService.deleteShopMainCat(id);
}
// =====================================================
// Shop Drawing Sub-Categories
// =====================================================
@Get('shop/sub-categories')
@ApiOperation({ summary: 'List Shop Drawing Sub-Categories' })
@ApiQuery({ name: 'projectId', required: true, type: Number })
@ApiQuery({ name: 'mainCategoryId', required: false, type: Number })
@RequirePermission('document.view')
getShopSubCats(@Query('mainCategoryId') mainCategoryId?: number) {
return this.masterDataService.findAllShopSubCats(mainCategoryId);
getShopSubCats(
@Query('projectId', ParseIntPipe) projectId: number,
@Query('mainCategoryId') mainCategoryId?: number
) {
return this.masterDataService.findAllShopSubCats(
projectId,
mainCategoryId ? Number(mainCategoryId) : undefined
);
}
@Post('shop/sub-categories')
@ApiOperation({ summary: 'Create Shop Sub-Category' })
@RequirePermission('master_data.drawing_category.manage')
createShopSubCat(
@Body()
body: {
projectId: number;
subCategoryCode: string;
subCategoryName: string;
description?: string;
isActive?: boolean;
sortOrder: number;
}
) {
return this.masterDataService.createShopSubCat(body);
}
@Patch('shop/sub-categories/:id')
@ApiOperation({ summary: 'Update Shop Sub-Category' })
@RequirePermission('master_data.drawing_category.manage')
updateShopSubCat(
@Param('id', ParseIntPipe) id: number,
@Body()
body: {
subCategoryCode?: string;
subCategoryName?: string;
description?: string;
isActive?: boolean;
sortOrder?: number;
}
) {
return this.masterDataService.updateShopSubCat(id, body);
}
@Delete('shop/sub-categories/:id')
@ApiOperation({ summary: 'Delete Shop Sub-Category' })
@RequirePermission('master_data.drawing_category.manage')
deleteShopSubCat(@Param('id', ParseIntPipe) id: number) {
return this.masterDataService.deleteShopSubCat(id);
}
}

View File

@@ -1,27 +1,36 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptionsWhere } from 'typeorm';
// Entities
import { ContractDrawingVolume } from './entities/contract-drawing-volume.entity';
import { ContractDrawingCategory } from './entities/contract-drawing-category.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';
import { ContractDrawingSubcatCatMap } from './entities/contract-drawing-subcat-cat-map.entity';
@Injectable()
export class DrawingMasterDataService {
constructor(
@InjectRepository(ContractDrawingVolume)
private cdVolumeRepo: Repository<ContractDrawingVolume>,
@InjectRepository(ContractDrawingCategory)
private cdCatRepo: Repository<ContractDrawingCategory>,
@InjectRepository(ContractDrawingSubCategory)
private cdSubCatRepo: Repository<ContractDrawingSubCategory>,
@InjectRepository(ShopDrawingMainCategory)
private sdMainCatRepo: Repository<ShopDrawingMainCategory>,
@InjectRepository(ShopDrawingSubCategory)
private sdSubCatRepo: Repository<ShopDrawingSubCategory>,
@InjectRepository(ContractDrawingSubcatCatMap)
private cdMapRepo: Repository<ContractDrawingSubcatCatMap>
) {}
// --- Contract Drawing Volumes ---
// =====================================================
// Contract Drawing Volumes
// =====================================================
async findAllVolumes(projectId: number) {
return this.cdVolumeRepo.find({
where: { projectId },
@@ -34,7 +43,54 @@ export class DrawingMasterDataService {
return this.cdVolumeRepo.save(volume);
}
// --- Contract Drawing Sub-Categories ---
async updateVolume(id: number, data: Partial<ContractDrawingVolume>) {
const volume = await this.cdVolumeRepo.findOne({ where: { id } });
if (!volume) throw new NotFoundException(`Volume #${id} not found`);
Object.assign(volume, data);
return this.cdVolumeRepo.save(volume);
}
async deleteVolume(id: number) {
const result = await this.cdVolumeRepo.delete(id);
if (result.affected === 0)
throw new NotFoundException(`Volume #${id} not found`);
return { deleted: true };
}
// =====================================================
// Contract Drawing Categories
// =====================================================
async findAllCategories(projectId: number) {
return this.cdCatRepo.find({
where: { projectId },
order: { sortOrder: 'ASC' },
});
}
async createCategory(data: Partial<ContractDrawingCategory>) {
const cat = this.cdCatRepo.create(data);
return this.cdCatRepo.save(cat);
}
async updateCategory(id: number, data: Partial<ContractDrawingCategory>) {
const cat = await this.cdCatRepo.findOne({ where: { id } });
if (!cat) throw new NotFoundException(`Category #${id} not found`);
Object.assign(cat, data);
return this.cdCatRepo.save(cat);
}
async deleteCategory(id: number) {
const result = await this.cdCatRepo.delete(id);
if (result.affected === 0)
throw new NotFoundException(`Category #${id} not found`);
return { deleted: true };
}
// =====================================================
// Contract Drawing Sub-Categories
// =====================================================
async findAllContractSubCats(projectId: number) {
return this.cdSubCatRepo.find({
where: { projectId },
@@ -47,26 +103,128 @@ export class DrawingMasterDataService {
return this.cdSubCatRepo.save(subCat);
}
// --- Shop Drawing Main Categories ---
async findAllShopMainCats() {
async updateContractSubCat(
id: number,
data: Partial<ContractDrawingSubCategory>
) {
const subCat = await this.cdSubCatRepo.findOne({ where: { id } });
if (!subCat) throw new NotFoundException(`Sub-Category #${id} not found`);
Object.assign(subCat, data);
return this.cdSubCatRepo.save(subCat);
}
async deleteContractSubCat(id: number) {
const result = await this.cdSubCatRepo.delete(id);
if (result.affected === 0)
throw new NotFoundException(`Sub-Category #${id} not found`);
return { deleted: true };
}
// =====================================================
// Contract Drawing Mappings (Category <-> Sub-Category)
// =====================================================
async findContractMappings(projectId: number, categoryId?: number) {
const where: FindOptionsWhere<ContractDrawingSubcatCatMap> = { projectId };
if (categoryId) {
where.categoryId = categoryId;
}
return this.cdMapRepo.find({
where,
relations: ['subCategory', 'category'],
order: { id: 'ASC' },
});
}
async createContractMapping(data: {
projectId: number;
categoryId: number;
subCategoryId: number;
}) {
// Check if mapping already exists to prevent duplicates (though DB has UNIQUE constraint)
const existing = await this.cdMapRepo.findOne({
where: {
projectId: data.projectId,
categoryId: data.categoryId,
subCategoryId: data.subCategoryId,
},
});
if (existing) return existing;
const map = this.cdMapRepo.create(data);
return this.cdMapRepo.save(map);
}
async deleteContractMapping(id: number) {
const result = await this.cdMapRepo.delete(id);
if (result.affected === 0)
throw new NotFoundException(`Mapping #${id} not found`);
return { deleted: true };
}
// =====================================================
// Shop Drawing Main Categories
// =====================================================
async findAllShopMainCats(projectId: number) {
return this.sdMainCatRepo.find({
where: { isActive: true },
where: { projectId },
order: { sortOrder: 'ASC' },
});
}
// --- Shop Drawing Sub Categories ---
async findAllShopSubCats(mainCategoryId?: number) {
// ✅ FIX: ใช้วิธี Spread Operator เพื่อสร้าง Object เงื่อนไขที่ถูกต้องตาม Type
async createShopMainCat(data: Partial<ShopDrawingMainCategory>) {
const cat = this.sdMainCatRepo.create(data);
return this.sdMainCatRepo.save(cat);
}
async updateShopMainCat(id: number, data: Partial<ShopDrawingMainCategory>) {
const cat = await this.sdMainCatRepo.findOne({ where: { id } });
if (!cat) throw new NotFoundException(`Main Category #${id} not found`);
Object.assign(cat, data);
return this.sdMainCatRepo.save(cat);
}
async deleteShopMainCat(id: number) {
const result = await this.sdMainCatRepo.delete(id);
if (result.affected === 0)
throw new NotFoundException(`Main Category #${id} not found`);
return { deleted: true };
}
// =====================================================
// Shop Drawing Sub-Categories
// =====================================================
async findAllShopSubCats(projectId: number, mainCategoryId?: number) {
const where: FindOptionsWhere<ShopDrawingSubCategory> = {
isActive: true,
projectId,
...(mainCategoryId ? { mainCategoryId } : {}),
};
return this.sdSubCatRepo.find({
where,
order: { sortOrder: 'ASC' },
relations: ['mainCategory'], // Load Parent Info
});
}
async createShopSubCat(data: Partial<ShopDrawingSubCategory>) {
const subCat = this.sdSubCatRepo.create(data);
return this.sdSubCatRepo.save(subCat);
}
async updateShopSubCat(id: number, data: Partial<ShopDrawingSubCategory>) {
const subCat = await this.sdSubCatRepo.findOne({ where: { id } });
if (!subCat) throw new NotFoundException(`Sub-Category #${id} not found`);
Object.assign(subCat, data);
return this.sdSubCatRepo.save(subCat);
}
async deleteShopSubCat(id: number) {
const result = await this.sdSubCatRepo.delete(id);
if (result.affected === 0)
throw new NotFoundException(`Sub-Category #${id} not found`);
return { deleted: true };
}
}

View File

@@ -5,6 +5,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ContractDrawing } from './entities/contract-drawing.entity';
import { ShopDrawing } from './entities/shop-drawing.entity';
import { ShopDrawingRevision } from './entities/shop-drawing-revision.entity';
import { AsBuiltDrawing } from './entities/asbuilt-drawing.entity';
import { AsBuiltDrawingRevision } from './entities/asbuilt-drawing-revision.entity';
// Entities (Master Data - Contract Drawing)
import { ContractDrawingVolume } from './entities/contract-drawing-volume.entity';
@@ -22,15 +24,19 @@ 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
import { AsBuiltDrawingService } from './asbuilt-drawing.service';
import { DrawingMasterDataService } from './drawing-master-data.service';
// Controllers
import { ShopDrawingController } from './shop-drawing.controller';
import { ContractDrawingController } from './contract-drawing.controller';
import { AsBuiltDrawingController } from './asbuilt-drawing.controller';
import { DrawingMasterDataController } from './drawing-master-data.controller';
// Modules
import { FileStorageModule } from '../../common/file-storage/file-storage.module';
import { UserModule } from '../user/user.module';
@Module({
imports: [
TypeOrmModule.forFeature([
@@ -38,14 +44,16 @@ import { UserModule } from '../user/user.module';
ContractDrawing,
ShopDrawing,
ShopDrawingRevision,
AsBuiltDrawing,
AsBuiltDrawingRevision,
// Master Data
ContractDrawingVolume,
ContractDrawingSubCategory,
ContractDrawingSubcatCatMap,
ContractDrawingCategory,
ShopDrawingMainCategory, // ✅
ShopDrawingSubCategory, // ✅
ShopDrawingMainCategory,
ShopDrawingSubCategory,
// Common
Attachment,
@@ -56,13 +64,15 @@ import { UserModule } from '../user/user.module';
providers: [
ShopDrawingService,
ContractDrawingService,
AsBuiltDrawingService,
DrawingMasterDataService,
],
controllers: [
ShopDrawingController,
ContractDrawingController,
AsBuiltDrawingController,
DrawingMasterDataController,
],
exports: [ShopDrawingService, ContractDrawingService],
exports: [ShopDrawingService, ContractDrawingService, AsBuiltDrawingService],
})
export class DrawingModule {}

View File

@@ -0,0 +1,50 @@
import {
IsNotEmpty,
IsOptional,
IsString,
IsArray,
IsDateString,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
/**
* DTO for creating a new revision to an existing AS Built Drawing
*/
export class CreateAsBuiltDrawingRevisionDto {
@ApiProperty({ description: 'Revision label (e.g., A, B, 1)' })
@IsString()
@IsNotEmpty()
revisionLabel!: string;
@ApiProperty({ description: 'Drawing title for this revision' })
@IsString()
@IsNotEmpty()
title!: string;
@ApiPropertyOptional({ description: 'Legacy/original drawing number' })
@IsString()
@IsOptional()
legacyDrawingNumber?: string;
@ApiPropertyOptional({ description: 'Revision date (ISO string)' })
@IsDateString()
@IsOptional()
revisionDate?: string;
@ApiPropertyOptional({ description: 'Description of changes' })
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({
description: 'Shop Drawing Revision IDs to reference',
})
@IsArray()
@IsOptional()
shopDrawingRevisionIds?: number[];
@ApiPropertyOptional({ description: 'Attachment IDs' })
@IsArray()
@IsOptional()
attachmentIds?: number[];
}

View File

@@ -0,0 +1,72 @@
import {
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsArray,
IsDateString,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
/**
* DTO for creating a new AS Built Drawing with its first revision
*/
export class CreateAsBuiltDrawingDto {
@ApiProperty({ description: 'Project ID' })
@IsNumber()
@IsNotEmpty()
projectId!: number;
@ApiProperty({ description: 'AS Built Drawing Number (unique)' })
@IsString()
@IsNotEmpty()
drawingNumber!: string;
@ApiProperty({ description: 'Main Category ID' })
@IsNumber()
@IsNotEmpty()
mainCategoryId!: number;
@ApiProperty({ description: 'Sub Category ID' })
@IsNumber()
@IsNotEmpty()
subCategoryId!: number;
// First Revision Data
@ApiProperty({ description: 'Drawing title' })
@IsString()
@IsNotEmpty()
title!: string;
@ApiPropertyOptional({ description: 'Revision label (e.g., A, B, 0)' })
@IsString()
@IsOptional()
revisionLabel?: string;
@ApiPropertyOptional({ description: 'Legacy/original drawing number' })
@IsString()
@IsOptional()
legacyDrawingNumber?: string;
@ApiPropertyOptional({ description: 'Revision date (ISO string)' })
@IsDateString()
@IsOptional()
revisionDate?: string;
@ApiPropertyOptional({ description: 'Description of the revision' })
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({
description: 'Shop Drawing Revision IDs to reference',
})
@IsArray()
@IsOptional()
shopDrawingRevisionIds?: number[];
@ApiPropertyOptional({ description: 'Attachment IDs' })
@IsArray()
@IsOptional()
attachmentIds?: number[];
}

View File

@@ -0,0 +1,44 @@
import { IsNumber, IsOptional, IsString, Min } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
/**
* DTO for searching/filtering AS Built Drawings
*/
export class SearchAsBuiltDrawingDto {
@ApiProperty({ description: 'Project ID' })
@Type(() => Number)
@IsNumber()
projectId!: number;
@ApiPropertyOptional({ description: 'Filter by Main Category ID' })
@Type(() => Number)
@IsNumber()
@IsOptional()
mainCategoryId?: number;
@ApiPropertyOptional({ description: 'Filter by Sub Category ID' })
@Type(() => Number)
@IsNumber()
@IsOptional()
subCategoryId?: number;
@ApiPropertyOptional({ description: 'Search by drawing number' })
@IsString()
@IsOptional()
search?: string;
@ApiPropertyOptional({ description: 'Page number', default: 1 })
@Type(() => Number)
@IsNumber()
@Min(1)
@IsOptional()
page?: number = 1;
@ApiPropertyOptional({ description: 'Items per page', default: 20 })
@Type(() => Number)
@IsNumber()
@Min(1)
@IsOptional()
limit?: number = 20;
}

View File

@@ -7,12 +7,15 @@ import {
JoinColumn,
ManyToMany,
JoinTable,
Unique,
} from 'typeorm';
import { AsBuiltDrawing } from './asbuilt-drawing.entity';
import { ShopDrawingRevision } from './shop-drawing-revision.entity';
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
import { User } from '../../user/entities/user.entity';
@Entity('asbuilt_drawing_revisions')
@Unique(['asBuiltDrawingId', 'isCurrent'])
export class AsBuiltDrawingRevision {
@PrimaryGeneratedColumn()
id!: number;
@@ -35,9 +38,26 @@ export class AsBuiltDrawingRevision {
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ name: 'legacy_drawing_number', length: 100, nullable: true })
legacyDrawingNumber?: string;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@Column({
name: 'is_current',
type: 'boolean',
nullable: true,
default: null,
})
isCurrent?: boolean | null;
@Column({ name: 'created_by', nullable: true })
createdBy?: number;
@Column({ name: 'updated_by', nullable: true })
updatedBy?: number;
// Relations
@ManyToOne(() => AsBuiltDrawing, (drawing) => drawing.revisions, {
onDelete: 'CASCADE',
@@ -45,6 +65,14 @@ export class AsBuiltDrawingRevision {
@JoinColumn({ name: 'asbuilt_drawing_id' })
asBuiltDrawing!: AsBuiltDrawing;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
creator?: User;
@ManyToOne(() => User)
@JoinColumn({ name: 'updated_by' })
updater?: User;
// Relation to Shop Drawing Revisions (M:N)
@ManyToMany(() => ShopDrawingRevision)
@JoinTable({

View File

@@ -12,6 +12,8 @@ import {
import { Project } from '../../project/entities/project.entity';
import { AsBuiltDrawingRevision } from './asbuilt-drawing-revision.entity';
import { User } from '../../user/entities/user.entity';
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
import { ShopDrawingSubCategory } from './shop-drawing-sub-category.entity';
@Entity('asbuilt_drawings')
export class AsBuiltDrawing {
@@ -24,6 +26,12 @@ export class AsBuiltDrawing {
@Column({ name: 'drawing_number', length: 100, unique: true })
drawingNumber!: string;
@Column({ name: 'main_category_id' })
mainCategoryId!: number;
@Column({ name: 'sub_category_id' })
subCategoryId!: number;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@@ -41,6 +49,14 @@ export class AsBuiltDrawing {
@JoinColumn({ name: 'project_id' })
project!: Project;
@ManyToOne(() => ShopDrawingMainCategory)
@JoinColumn({ name: 'main_category_id' })
mainCategory!: ShopDrawingMainCategory;
@ManyToOne(() => ShopDrawingSubCategory)
@JoinColumn({ name: 'sub_category_id' })
subCategory!: ShopDrawingSubCategory;
@ManyToOne(() => User)
@JoinColumn({ name: 'updated_by' })
updater?: User;

View File

@@ -7,12 +7,15 @@ import {
JoinColumn,
ManyToMany,
JoinTable,
Unique,
} from 'typeorm';
import { ShopDrawing } from './shop-drawing.entity';
import { ContractDrawing } from './contract-drawing.entity';
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
import { User } from '../../user/entities/user.entity';
@Entity('shop_drawing_revisions')
@Unique(['shopDrawingId', 'isCurrent'])
export class ShopDrawingRevision {
@PrimaryGeneratedColumn()
id!: number; // เติม !
@@ -41,6 +44,20 @@ export class ShopDrawingRevision {
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date; // เติม !
@Column({
name: 'is_current',
type: 'boolean',
nullable: true,
default: null,
})
isCurrent?: boolean | null;
@Column({ name: 'created_by', nullable: true })
createdBy?: number;
@Column({ name: 'updated_by', nullable: true })
updatedBy?: number;
// Relations
@ManyToOne(() => ShopDrawing, (shopDrawing) => shopDrawing.revisions, {
onDelete: 'CASCADE',
@@ -48,6 +65,14 @@ export class ShopDrawingRevision {
@JoinColumn({ name: 'shop_drawing_id' })
shopDrawing!: ShopDrawing; // เติม !
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
creator?: User;
@ManyToOne(() => User)
@JoinColumn({ name: 'updated_by' })
updater?: User;
// References to Contract Drawings (M:N)
@ManyToMany(() => ContractDrawing)
@JoinTable({