251225:1703 On going update to 1.7.0: Refoctory drawing Module not finish
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
101
backend/src/modules/drawing/asbuilt-drawing.controller.ts
Normal file
101
backend/src/modules/drawing/asbuilt-drawing.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
307
backend/src/modules/drawing/asbuilt-drawing.service.ts
Normal file
307
backend/src/modules/drawing/asbuilt-drawing.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user