260321:1700 Correct Coresspondence / Doing RFA

This commit is contained in:
admin
2026-03-21 17:00:41 +07:00
parent dcf55f4d08
commit 03d16cfd64
57 changed files with 1923 additions and 663 deletions
+4 -4
View File
@@ -4,10 +4,10 @@ services:
container_name: mariadb-local
restart: always
environment:
MYSQL_ROOT_PASSWORD: Center#2025
MYSQL_ROOT_PASSWORD: Center2025
MYSQL_DATABASE: lcbp3_dev
MYSQL_USER: admin
MYSQL_PASSWORD: Center#2025
MYSQL_PASSWORD: Center2025
ports:
- '3306:3306'
volumes:
@@ -47,9 +47,9 @@ services:
environment:
- discovery.type=single-node
- xpack.security.enabled=false # ปิด security เพื่อความง่ายใน Dev (Prod ต้องเปิด)
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- 'ES_JAVA_OPTS=-Xms512m -Xmx512m'
ports:
- "9200:9200"
- '9200:9200'
volumes:
- esdata:/usr/share/elasticsearch/data
networks:
+3 -3
View File
@@ -25,7 +25,7 @@
},
"dependencies": {
"@casl/ability": "^6.7.5",
"@elastic/elasticsearch": "^9.3.4",
"@elastic/elasticsearch": "^8.13.0",
"@nestjs-modules/ioredis": "^2.0.2",
"@nestjs/axios": "^4.0.1",
"@nestjs/bullmq": "^11.0.4",
@@ -61,9 +61,9 @@
"helmet": "^8.1.0",
"ioredis": "^5.8.2",
"joi": "^18.0.1",
"ms": "^2.1.3",
"multer": "^2.0.2",
"mysql2": "^3.15.3",
"ms": "^2.1.3",
"nest-winston": "^1.10.2",
"nodemailer": "^8.0.3",
"opossum": "^9.0.0",
@@ -92,8 +92,8 @@
"@types/express": "^5.0.0",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^30.0.0",
"@types/multer": "^2.0.0",
"@types/ms": "^2.1.0",
"@types/multer": "^2.0.0",
"@types/node": "^25.5.0",
"@types/opossum": "^8.1.9",
"@types/passport-jwt": "^4.0.1",
@@ -102,4 +102,32 @@ export class UuidResolverService {
async resolveContractId(contractId: number | string): Promise<number> {
return this.resolve('Contract', 'contracts', 'id', contractId);
}
/**
* Resolve shopDrawingRevisionId (INT or UUID string) to internal INT ID.
*/
async resolveShopDrawingRevisionId(
shopDrawingRevisionId: number | string
): Promise<number> {
return this.resolve(
'Shop Drawing Revision',
'shop_drawing_revisions',
'id',
shopDrawingRevisionId
);
}
/**
* Resolve asBuiltDrawingRevisionId (INT or UUID string) to internal INT ID.
*/
async resolveAsBuiltDrawingRevisionId(
asBuiltDrawingRevisionId: number | string
): Promise<number> {
return this.resolve(
'As-Built Drawing Revision',
'asbuilt_drawing_revisions',
'id',
asBuiltDrawingRevisionId
);
}
}
@@ -207,6 +207,9 @@ export class CorrespondenceService {
issuedDate: createDto.issuedDate
? new Date(createDto.issuedDate)
: undefined,
receivedDate: createDto.receivedDate
? new Date(createDto.receivedDate)
: undefined,
description: createDto.description,
details: createDto.details,
createdBy: user.user_id,
@@ -521,6 +524,8 @@ export class CorrespondenceService {
revisionUpdate.documentDate = new Date(updateDto.documentDate);
if (updateDto.issuedDate)
revisionUpdate.issuedDate = new Date(updateDto.issuedDate);
if (updateDto.receivedDate)
revisionUpdate.receivedDate = new Date(updateDto.receivedDate);
if (updateDto.description)
revisionUpdate.description = updateDto.description;
if (updateDto.details) revisionUpdate.details = updateDto.details;
@@ -99,6 +99,14 @@ export class CreateCorrespondenceDto {
@IsOptional()
issuedDate?: string;
@ApiPropertyOptional({
description: 'Received Date (วันที่รับเอกสาร)',
example: '2025-12-06T00:00:00Z',
})
@IsDateString()
@IsOptional()
receivedDate?: string;
@ApiPropertyOptional({
description: 'Attachment temp IDs from upload phase (Two-Phase Storage)',
example: ['uuid-temp-1', 'uuid-temp-2'],
@@ -32,7 +32,7 @@ export class DocumentNumberingAdminController {
@Get('templates')
@ApiOperation({ summary: 'Get all document numbering templates' })
@RequirePermission('system.manage_settings')
@RequirePermission('numbering.view_formats')
async getTemplates(@Query('projectId') projectId?: number) {
if (projectId) {
return this.service.getTemplatesByProject(projectId);
@@ -42,7 +42,7 @@ export class DocumentNumberingAdminController {
@Post('templates')
@ApiOperation({ summary: 'Create or Update a numbering template' })
@RequirePermission('system.manage_settings')
@RequirePermission('numbering.manage_formats')
async saveTemplate(
@Body() dto: Partial<DocumentNumberFormat> & { projectId?: number | string }
) {
@@ -51,7 +51,7 @@ export class DocumentNumberingAdminController {
@Delete('templates/:id')
@ApiOperation({ summary: 'Delete a numbering template' })
@RequirePermission('system.manage_settings')
@RequirePermission('numbering.manage_formats')
async deleteTemplate(@Param('id', ParseIntPipe) id: number) {
await this.service.deleteTemplate(id);
return { success: true };
@@ -78,7 +78,7 @@ export class DocumentNumberingAdminController {
@ApiOperation({
summary: 'Manually override or set a document number counter',
})
@RequirePermission('system.manage_settings')
@RequirePermission('numbering.manage_formats')
async manualOverride(
@Body() dto: ManualOverrideDto,
@CurrentUser() user: User
@@ -88,7 +88,7 @@ export class DocumentNumberingAdminController {
@Post('void-and-replace')
@ApiOperation({ summary: 'Void a number and replace with a new generation' })
@RequirePermission('system.manage_settings')
@RequirePermission('numbering.manage_formats')
async voidAndReplace(
@Body()
dto: {
@@ -104,7 +104,7 @@ export class DocumentNumberingAdminController {
@Post('cancel')
@ApiOperation({ summary: 'Cancel/Skip a specific document number' })
@RequirePermission('system.manage_settings')
@RequirePermission('numbering.manage_formats')
async cancelNumber(
@Body()
dto: {
@@ -119,7 +119,7 @@ export class DocumentNumberingAdminController {
@Post('bulk-import')
@ApiOperation({ summary: 'Bulk import/set document number counters' })
@RequirePermission('system.manage_settings')
@RequirePermission('numbering.manage_formats')
async bulkImport(@Body() items: ManualOverrideDto[]) {
return this.service.bulkImport(items);
}
@@ -68,7 +68,7 @@ export class DocumentNumberingController {
@Patch('counters/:id')
@ApiOperation({ summary: 'Update counter sequence value (Admin only)' })
@RequirePermission('system.manage_settings')
@RequirePermission('numbering.manage_formats')
async updateCounter(
@Param('id', ParseIntPipe) id: number,
@Body('sequence') sequence: number
@@ -105,7 +105,7 @@ export class DocumentNumberingController {
)
: undefined;
return this.numberingService.previewNumber({
const result = await this.numberingService.previewNumber({
projectId: resolvedProjectId,
originatorOrganizationId: resolvedOriginatorId,
typeId: dto.correspondenceTypeId,
@@ -116,5 +116,7 @@ export class DocumentNumberingController {
year: dto.year,
customTokens: dto.customTokens,
});
console.log('[DocumentNumberingController] Preview result:', JSON.stringify(result));
return result;
}
}
@@ -14,7 +14,7 @@ import { Project } from '../../project/entities/project.entity';
import { CorrespondenceType } from '../../correspondence/entities/correspondence-type.entity';
@Entity('document_number_formats')
@Unique(['projectId', 'correspondenceTypeId'])
@Unique(['projectId', 'correspondenceTypeId', 'disciplineId'])
export class DocumentNumberFormat {
@PrimaryGeneratedColumn()
id!: number;
@@ -25,6 +25,9 @@ export class DocumentNumberFormat {
@Column({ name: 'correspondence_type_id', nullable: true })
correspondenceTypeId?: number;
@Column({ name: 'discipline_id', default: 0 })
disciplineId!: number;
@Column({ name: 'format_string', length: 100 })
formatTemplate!: string;
@@ -35,6 +38,9 @@ export class DocumentNumberFormat {
@Column({ name: 'reset_annually', default: true })
resetSequenceYearly!: boolean;
@Column({ name: 'is_active', default: 1 })
isActive!: number;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@@ -5,7 +5,13 @@ import {
NotFoundException,
} from '@nestjs/common';
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
import { Repository, EntityManager } from 'typeorm';
import {
Repository,
EntityManager,
In,
IsNull,
Equal,
} from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { DocumentNumberFormat } from '../entities/document-number-format.entity';
@@ -121,7 +127,7 @@ export class DocumentNumberingService {
const sequence = await this.counterService.incrementCounter(key);
// 4. Format Number
const documentNumber = await this.formatService.format({
const { previewNumber: documentNumber } = await this.formatService.format({
projectId: ctx.projectId,
correspondenceTypeId: ctx.typeId,
subTypeId: ctx.subTypeId,
@@ -193,7 +199,7 @@ export class DocumentNumberingService {
async previewNumber(
ctx: GenerateNumberContext
): Promise<{ previewNumber: string; nextSequence: number }> {
): Promise<{ previewNumber: string; nextSequence: number; isDefault: boolean }> {
const currentYear = new Date().getFullYear();
const resetScope = `YEAR_${currentYear}`;
@@ -211,7 +217,7 @@ export class DocumentNumberingService {
const currentSeq = await this.counterService.getCurrentCounter(key);
const nextSequence = currentSeq + 1;
const previewNumber = await this.formatService.format({
const { previewNumber, isDefault } = await this.formatService.format({
projectId: ctx.projectId,
correspondenceTypeId: ctx.typeId,
subTypeId: ctx.subTypeId,
@@ -224,7 +230,7 @@ export class DocumentNumberingService {
recipientOrganizationId: ctx.recipientOrganizationId,
});
return { previewNumber, nextSequence };
return { previewNumber, nextSequence, isDefault };
}
/**
@@ -258,10 +264,36 @@ export class DocumentNumberingService {
async saveTemplate(
dto: Partial<DocumentNumberFormat> & { projectId?: number | string }
) {
if (dto.projectId) {
dto.projectId = await this.uuidResolver.resolveProjectId(dto.projectId);
try {
this.logger.log(`Saving numbering template: ${JSON.stringify(dto)}`);
// Resolve project ID if it's a UUID/String
if (dto.projectId && typeof dto.projectId === 'string') {
dto.projectId = await this.uuidResolver.resolveProjectId(dto.projectId);
}
// Upsert logic: If no ID provided, check for existing template with same business key
if (!dto.id) {
const existing = await this.formatRepo.findOne({
where: {
projectId: Number(dto.projectId),
correspondenceTypeId: dto.correspondenceTypeId ? Equal(dto.correspondenceTypeId) : IsNull(),
disciplineId: dto.disciplineId || 0,
},
});
if (existing) {
this.logger.log(`Found existing template ID: ${existing.id} for business key, updating instead of creating.`);
dto.id = existing.id;
}
}
const result = await this.formatRepo.save(dto);
this.logger.log(`Successfully saved template ID: ${result.id}`);
return result;
} catch (e: any) {
this.logger.error(`Failed to save numbering template: ${e.message}`, e.stack);
throw e;
}
return this.formatRepo.save(dto);
}
async deleteTemplate(id: number) {
@@ -39,12 +39,14 @@ export class FormatService {
private disciplineRepo: Repository<Discipline>
) {}
async format(options: FormatOptions): Promise<string> {
const { template } = await this.resolveFormatAndScope(options);
async format(options: FormatOptions): Promise<{ previewNumber: string; isDefault: boolean }> {
const { template, isDefault } = await this.resolveFormatAndScope(options);
const currentYear = options.year || new Date().getFullYear();
const tokens = await this.resolveTokens(options, currentYear);
return this.replaceTokens(template, tokens, options.sequence);
const previewNumber = this.replaceTokens(template, tokens, options.sequence);
console.log(`[FormatService] Generated: "${previewNumber}" | Template: "${template}" | isDefault: ${isDefault}`);
return { previewNumber, isDefault };
}
// --- Helpers ---
@@ -52,6 +54,7 @@ export class FormatService {
private async resolveFormatAndScope(options: FormatOptions): Promise<{
template: string;
resetSequenceYearly: boolean;
isDefault: boolean;
}> {
// 1. Specific Format
const specificFormat = await this.formatRepo.findOne({
@@ -64,6 +67,7 @@ export class FormatService {
return {
template: specificFormat.formatTemplate,
resetSequenceYearly: specificFormat.resetSequenceYearly,
isDefault: false,
};
// 2. Default Format
@@ -74,12 +78,14 @@ export class FormatService {
return {
template: defaultFormat.formatTemplate,
resetSequenceYearly: defaultFormat.resetSequenceYearly,
isDefault: true,
};
// 3. Fallback
return {
template: '{ORG}-{RECIPIENT}-{SEQ:4}-{YEAR:BE}',
resetSequenceYearly: true,
isDefault: true,
};
}
@@ -61,7 +61,7 @@ export class ReservationService {
const sequence = await this.counterService.incrementCounter(counterKey);
// Format document number
const documentNumber = await this.formatService.format({
const { previewNumber: documentNumber } = await this.formatService.format({
...dto,
sequence,
resetScope: counterKey.resetScope,
@@ -12,6 +12,15 @@ export class CreateTagDto {
@IsOptional()
description?: string;
@ApiProperty({
example: 'red',
description: 'รหัสสี หรือชื่อคลาสสำหรับ UI',
required: false,
})
@IsString()
@IsOptional()
color_code?: string;
@ApiProperty({
example: 1,
description: 'Project ID or UUID',
@@ -86,13 +86,21 @@ export class CreateRfaRevisionDto {
})
@IsObject()
@IsOptional()
details?: Record<string, any>;
details?: Record<string, unknown>;
@ApiPropertyOptional({
description: 'Linked Shop Drawing Revision IDs',
example: [1, 2],
description: 'Linked Shop Drawing Revision IDs or UUIDs',
example: ['shop-revision-uuid-1', 'shop-revision-uuid-2'],
})
@IsArray()
@IsOptional()
shopDrawingRevisionIds?: number[]; // IDs of linked Shop Drawings
shopDrawingRevisionIds?: Array<number | string>;
@ApiPropertyOptional({
description: 'Linked As-Built Drawing Revision IDs or UUIDs',
example: ['asbuilt-revision-uuid-1'],
})
@IsArray()
@IsOptional()
asBuiltDrawingRevisionIds?: Array<number | string>;
}
+16 -7
View File
@@ -20,9 +20,9 @@ export class CreateRfaDto {
@IsOptional()
contractId?: string;
@ApiProperty({ description: 'To Organization ID or UUID', required: false })
@IsOptional()
toOrganizationId?: number | string;
@ApiProperty({ description: 'To Organization ID or UUID' })
@IsNotEmpty()
toOrganizationId!: number | string;
@ApiProperty({ description: 'ID ของประเภท RFA', example: 1 })
@IsInt()
@@ -76,14 +76,23 @@ export class CreateRfaDto {
})
@IsObject()
@IsOptional()
details?: Record<string, any>;
details?: Record<string, unknown>;
@ApiProperty({
description: 'รายการ Shop Drawing Revisions ที่แนบมาด้วย',
description: 'รายการ Shop Drawing Revision IDs หรือ UUIDs ที่แนบมาด้วย',
required: false,
type: [Number],
type: [String],
})
@IsArray()
@IsOptional()
shopDrawingRevisionIds?: number[];
shopDrawingRevisionIds?: Array<number | string>;
@ApiProperty({
description: 'รายการ As-Built Drawing Revision IDs หรือ UUIDs ที่แนบมาด้วย',
required: false,
type: [String],
})
@IsArray()
@IsOptional()
asBuiltDrawingRevisionIds?: Array<number | string>;
}
@@ -1,14 +1,34 @@
import { Entity, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { RfaRevision } from './rfa-revision.entity';
import { AsBuiltDrawingRevision } from '../../drawing/entities/asbuilt-drawing-revision.entity';
import { ShopDrawingRevision } from '../../drawing/entities/shop-drawing-revision.entity';
@Entity('rfa_items')
export class RfaItem {
@PrimaryColumn({ name: 'rfa_revision_id' })
@PrimaryGeneratedColumn()
id!: number;
@Column({ name: 'rfa_revision_id' })
rfaRevisionId!: number;
@PrimaryColumn({ name: 'shop_drawing_revision_id' })
shopDrawingRevisionId!: number;
@Column({
name: 'item_type',
type: 'enum',
enum: ['SHOP', 'AS_BUILT'],
})
itemType!: 'SHOP' | 'AS_BUILT';
@Column({ name: 'shop_drawing_revision_id', nullable: true })
shopDrawingRevisionId?: number;
@Column({ name: 'asbuilt_drawing_revision_id', nullable: true })
asBuiltDrawingRevisionId?: number;
// Relations
@ManyToOne(() => RfaRevision, (rfaRev) => rfaRev.items, {
@@ -19,5 +39,9 @@ export class RfaItem {
@ManyToOne(() => ShopDrawingRevision)
@JoinColumn({ name: 'shop_drawing_revision_id' })
shopDrawingRevision!: ShopDrawingRevision;
shopDrawingRevision?: ShopDrawingRevision;
@ManyToOne(() => AsBuiltDrawingRevision)
@JoinColumn({ name: 'asbuilt_drawing_revision_id' })
asBuiltDrawingRevision?: AsBuiltDrawingRevision;
}
+6
View File
@@ -7,10 +7,13 @@ import { CorrespondenceRouting } from '../correspondence/entities/correspondence
import { Correspondence } from '../correspondence/entities/correspondence.entity';
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
import { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity';
import { RoutingTemplate } from '../correspondence/entities/routing-template.entity';
import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity';
import { AsBuiltDrawingRevision } from '../drawing/entities/asbuilt-drawing-revision.entity';
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity';
import { Discipline } from '../master/entities/discipline.entity';
import { RfaApproveCode } from './entities/rfa-approve-code.entity';
import { RfaItem } from './entities/rfa-item.entity';
import { RfaRevision } from './entities/rfa-revision.entity';
@@ -46,7 +49,10 @@ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module'
Correspondence,
CorrespondenceRevision,
CorrespondenceStatus,
CorrespondenceType,
AsBuiltDrawingRevision,
ShopDrawingRevision,
Discipline,
RfaWorkflow,
RfaWorkflowTemplate,
RfaWorkflowTemplateStep,
+168 -15
View File
@@ -13,12 +13,16 @@ import { DataSource, In, Repository } from 'typeorm';
// Entities
import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity';
import { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity';
import { Correspondence } from '../correspondence/entities/correspondence.entity';
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
import { RoutingTemplate } from '../correspondence/entities/routing-template.entity';
import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity';
import { AsBuiltDrawingRevision } from '../drawing/entities/asbuilt-drawing-revision.entity';
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity';
import { Discipline } from '../master/entities/discipline.entity';
import { User } from '../user/entities/user.entity';
import { RfaApproveCode } from './entities/rfa-approve-code.entity';
import { RfaItem } from './entities/rfa-item.entity';
@@ -72,10 +76,16 @@ export class RfaService {
private corrRevRepo: Repository<CorrespondenceRevision>,
@InjectRepository(CorrespondenceStatus)
private corrStatusRepo: Repository<CorrespondenceStatus>,
@InjectRepository(CorrespondenceType)
private correspondenceTypeRepo: Repository<CorrespondenceType>,
@InjectRepository(Discipline)
private disciplineRepo: Repository<Discipline>,
@InjectRepository(RfaStatusCode)
private rfaStatusRepo: Repository<RfaStatusCode>,
@InjectRepository(RfaApproveCode)
private rfaApproveRepo: Repository<RfaApproveCode>,
@InjectRepository(AsBuiltDrawingRevision)
private asBuiltDrawingRevRepo: Repository<AsBuiltDrawingRevision>,
@InjectRepository(ShopDrawingRevision)
private shopDrawingRevRepo: Repository<ShopDrawingRevision>,
@InjectRepository(CorrespondenceRouting)
@@ -105,6 +115,104 @@ export class RfaService {
});
if (!rfaType) throw new NotFoundException('RFA Type not found');
const rfaTypeCode = rfaType.typeCode.toUpperCase();
const rawShopDrawingRefs = createDto.shopDrawingRevisionIds ?? [];
const rawAsBuiltDrawingRefs = createDto.asBuiltDrawingRevisionIds ?? [];
if (['DDW', 'SDW'].includes(rfaTypeCode)) {
if (rawShopDrawingRefs.length === 0) {
throw new BadRequestException(
'Selected RFA Type requires at least one Shop Drawing Revision'
);
}
if (rawAsBuiltDrawingRefs.length > 0) {
throw new BadRequestException(
'Selected RFA Type cannot reference As-Built Drawing Revisions'
);
}
} else if (rfaTypeCode === 'ADW') {
if (rawAsBuiltDrawingRefs.length === 0) {
throw new BadRequestException(
'Selected RFA Type requires at least one As-Built Drawing Revision'
);
}
if (rawShopDrawingRefs.length > 0) {
throw new BadRequestException(
'Selected RFA Type cannot reference Shop Drawing Revisions'
);
}
} else if (
rawShopDrawingRefs.length > 0 ||
rawAsBuiltDrawingRefs.length > 0
) {
throw new BadRequestException(
'Selected RFA Type does not support drawing revision items'
);
}
const shopDrawingRevisionIds = Array.from(
new Set(
await Promise.all(
rawShopDrawingRefs.map((ref) =>
this.uuidResolver.resolveShopDrawingRevisionId(ref)
)
)
)
);
const asBuiltDrawingRevisionIds = Array.from(
new Set(
await Promise.all(
rawAsBuiltDrawingRefs.map((ref) =>
this.uuidResolver.resolveAsBuiltDrawingRevisionId(ref)
)
)
)
);
const correspondenceType = await this.correspondenceTypeRepo.findOne({
where: { typeCode: 'RFA', isActive: true },
});
if (!correspondenceType) {
throw new InternalServerErrorException(
'Correspondence Type RFA not found in Master Data'
);
}
const internalContractId = createDto.contractId
? await this.uuidResolver.resolveContractId(createDto.contractId)
: rfaType.contractId;
if (rfaType.contractId !== internalContractId) {
throw new BadRequestException(
'Selected RFA Type does not belong to the selected contract'
);
}
if (createDto.disciplineId) {
const discipline = await this.disciplineRepo.findOne({
where: { id: createDto.disciplineId },
});
if (!discipline) {
throw new NotFoundException('Discipline not found');
}
if (discipline.contractId !== internalContractId) {
throw new BadRequestException(
'Selected Discipline does not belong to the selected contract'
);
}
}
const internalRecipientOrgId = createDto.toOrganizationId
? await this.uuidResolver.resolveOrganizationId(
createDto.toOrganizationId
)
: undefined;
const statusDraft = await this.rfaStatusRepo.findOne({
where: { statusCode: 'DFT' },
});
@@ -135,7 +243,9 @@ export class RfaService {
const docNumber = await this.numberingService.generateNextNumber({
projectId: internalProjectId,
originatorOrganizationId: userOrgId,
typeId: createDto.rfaTypeId,
recipientOrganizationId: internalRecipientOrgId,
typeId: correspondenceType.id,
rfaTypeId: createDto.rfaTypeId,
disciplineId: createDto.disciplineId ?? 0, // ✅ ส่ง disciplineId ไปด้วย (0 ถ้าไม่มี)
year: new Date().getFullYear(),
customTokens: {
@@ -159,7 +269,7 @@ export class RfaService {
// 1. Create Correspondence Record
const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber.number,
correspondenceTypeId: createDto.rfaTypeId,
correspondenceTypeId: correspondenceType.id,
projectId: internalProjectId,
originatorId: userOrgId,
isInternal: false,
@@ -168,6 +278,15 @@ export class RfaService {
});
const savedCorr = await queryRunner.manager.save(correspondence);
if (internalRecipientOrgId) {
const recipient = queryRunner.manager.create(CorrespondenceRecipient, {
correspondenceId: savedCorr.id,
recipientOrganizationId: internalRecipientOrgId,
recipientType: 'TO',
});
await queryRunner.manager.save(recipient);
}
// 2. Create Rfa Master Record
const rfa = queryRunner.manager.create(Rfa, {
id: savedCorr.id, // ✅ CTI Key share
@@ -205,25 +324,51 @@ export class RfaService {
});
const savedRevision = await queryRunner.manager.save(rfaRevision);
// 4. Link Shop Drawings
if (
createDto.shopDrawingRevisionIds &&
createDto.shopDrawingRevisionIds.length > 0
) {
const rfaItems: RfaItem[] = [];
if (shopDrawingRevisionIds.length > 0) {
const shopDrawings = await this.shopDrawingRevRepo.findBy({
id: In(createDto.shopDrawingRevisionIds),
id: In(shopDrawingRevisionIds),
});
if (shopDrawings.length !== createDto.shopDrawingRevisionIds.length) {
if (shopDrawings.length !== shopDrawingRevisionIds.length) {
throw new NotFoundException('Some Shop Drawing Revisions not found');
}
const rfaItems = shopDrawings.map((sd) =>
queryRunner.manager.create(RfaItem, {
rfaRevisionId: savedRevision.id, // Correctly link to RfaRevision
shopDrawingRevisionId: sd.id,
})
rfaItems.push(
...shopDrawings.map((sd) =>
queryRunner.manager.create(RfaItem, {
rfaRevisionId: savedRevision.id,
itemType: 'SHOP',
shopDrawingRevisionId: sd.id,
})
)
);
}
if (asBuiltDrawingRevisionIds.length > 0) {
const asBuiltDrawings = await this.asBuiltDrawingRevRepo.findBy({
id: In(asBuiltDrawingRevisionIds),
});
if (asBuiltDrawings.length !== asBuiltDrawingRevisionIds.length) {
throw new NotFoundException(
'Some As-Built Drawing Revisions not found'
);
}
rfaItems.push(
...asBuiltDrawings.map((ad) =>
queryRunner.manager.create(RfaItem, {
rfaRevisionId: savedRevision.id,
itemType: 'AS_BUILT',
asBuiltDrawingRevisionId: ad.id,
})
)
);
}
if (rfaItems.length > 0) {
await queryRunner.manager.save(rfaItems);
}
@@ -303,7 +448,11 @@ export class RfaService {
.leftJoinAndSelect('rfaRev.statusCode', 'status')
.leftJoinAndSelect('rfaRev.items', 'items')
.leftJoinAndSelect('items.shopDrawingRevision', 'sdRev')
.leftJoinAndSelect('sdRev.attachments', 'attachments');
.leftJoinAndSelect('sdRev.shopDrawing', 'shopDrawing')
.leftJoinAndSelect('sdRev.attachments', 'shopAttachments')
.leftJoinAndSelect('items.asBuiltDrawingRevision', 'adRev')
.leftJoinAndSelect('adRev.asBuiltDrawing', 'asBuiltDrawing')
.leftJoinAndSelect('adRev.attachments', 'asBuiltAttachments');
// Filter by Revision Status (from query param 'revisionStatus')
if (revisionStatus === 'CURRENT') {
@@ -405,6 +554,10 @@ export class RfaService {
'correspondence.revisions.rfaRevision.items',
'correspondence.revisions.rfaRevision.items.shopDrawingRevision',
'correspondence.revisions.rfaRevision.items.shopDrawingRevision.shopDrawing',
'correspondence.revisions.rfaRevision.items.shopDrawingRevision.attachments',
'correspondence.revisions.rfaRevision.items.asBuiltDrawingRevision',
'correspondence.revisions.rfaRevision.items.asBuiltDrawingRevision.asBuiltDrawing',
'correspondence.revisions.rfaRevision.items.asBuiltDrawingRevision.attachments',
],
order: {
correspondence: { revisions: { revisionNumber: 'DESC' } },