260310:1705 20260310:1700 Refactor rfas
Build and Deploy / deploy (push) Successful in 5m42s

This commit is contained in:
admin
2026-03-10 17:05:30 +07:00
parent b6dc83d54a
commit 15b447ceeb
22 changed files with 3086 additions and 380 deletions
@@ -7,7 +7,9 @@ import {
JoinColumn,
CreateDateColumn,
Index,
OneToOne,
} from 'typeorm';
import { RfaRevision } from '../../rfa/entities/rfa-revision.entity';
import { Correspondence } from './correspondence.entity';
import { CorrespondenceStatus } from './correspondence-status.entity';
import { User } from '../../user/entities/user.entity';
@@ -107,4 +109,8 @@ export class CorrespondenceRevision {
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
creator?: User;
// Added inverse relation for CTI mapping to subclasses (RFA)
@OneToOne(() => RfaRevision, (rfaRev) => rfaRev.correspondenceRevision)
rfaRevision?: RfaRevision;
}
@@ -54,6 +54,10 @@ export class ImportCorrespondenceDto {
@IsOptional()
received_date?: string;
@IsNumber()
@IsOptional()
discipline_id?: number;
@IsString()
@IsOptional()
body?: string;
@@ -127,22 +127,25 @@ export class MigrationService {
correspondenceNumber: dto.document_number,
correspondenceTypeId: typeId,
projectId: project.id,
disciplineId: dto.discipline_id || undefined,
isInternal: false,
createdBy: userId,
});
await queryRunner.manager.save(correspondence);
} else if (dto.discipline_id && !correspondence.disciplineId) {
// อัพเดต discipline_id หากเอกสารเดิมยังไม่มี
correspondence.disciplineId = dto.discipline_id;
await queryRunner.manager.save(correspondence);
}
// 4. File Handling
// We will map the source file and create an Attachment record using FileStorageService
// For legacy migrations, we pass document_number mapping logic or basic processing
let attachmentId: number | null = null;
if (dto.source_file_path) {
try {
const attachment = await this.fileStorageService.importStagingFile(
dto.source_file_path,
userId,
{ documentType: dto.category } // use category from DTO directly
{ documentType: dto.category }
);
attachmentId = attachment.id;
} catch (fileError: unknown) {
@@ -163,7 +166,6 @@ export class MigrationService {
}
);
// Determine revision number. Support mapping multiple batches to the same document number by incrementing revision.
const revNum = revisionCount;
const revision = queryRunner.manager.create(CorrespondenceRevision, {
correspondenceId: correspondence.id,
@@ -173,15 +175,16 @@ export class MigrationService {
statusId: status.id,
subject: dto.title,
description: 'Migrated from legacy system via Auto Ingest',
body: dto.body || undefined, // Map from DTO
details: {
...dto.details,
ai_confidence: dto.ai_confidence,
ai_issues: dto.ai_issues as unknown,
source_file_path: dto.source_file_path,
attachment_id: attachmentId, // Link attachment ID if successful
attachment_id: attachmentId,
},
schemaVersion: 1,
createdBy: userId, // Bot ID
createdBy: userId,
});
if (revisionCount > 0) {
@@ -1,39 +1,27 @@
// File: src/modules/rfa/entities/rfa-revision.entity.ts
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Unique,
PrimaryColumn,
OneToOne,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { CorrespondenceRevision } from '../../correspondence/entities/correspondence-revision.entity';
import { RfaApproveCode } from './rfa-approve-code.entity';
import { RfaItem } from './rfa-item.entity';
import { RfaStatusCode } from './rfa-status-code.entity';
import { RfaWorkflow } from './rfa-workflow.entity';
import { Rfa } from './rfa.entity';
@Entity('rfa_revisions')
@Unique(['rfaId', 'revisionNumber'])
@Unique(['rfaId', 'isCurrent'])
export class RfaRevision {
@PrimaryGeneratedColumn()
@PrimaryColumn()
id!: number;
@Column({ name: 'rfa_id' })
rfaId!: number;
@Column({ name: 'revision_number' })
revisionNumber!: number;
@Column({ name: 'revision_label', length: 10, nullable: true })
revisionLabel?: string;
@Column({ name: 'is_current', default: false })
isCurrent!: boolean;
@OneToOne(() => CorrespondenceRevision, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id' })
correspondenceRevision!: CorrespondenceRevision;
@Column({ name: 'rfa_status_code_id' })
rfaStatusCodeId!: number;
@@ -41,33 +29,9 @@ export class RfaRevision {
@Column({ name: 'rfa_approve_code_id', nullable: true })
rfaApproveCodeId?: number;
@Column({ length: 500 })
subject!: string;
@Column({ name: 'document_date', type: 'date', nullable: true })
documentDate?: Date;
@Column({ name: 'issued_date', type: 'date', nullable: true })
issuedDate?: Date;
@Column({ name: 'received_date', type: 'datetime', nullable: true })
receivedDate?: Date;
@Column({ name: 'due_date', type: 'datetime', nullable: true })
dueDate?: Date;
@Column({ name: 'approved_date', type: 'date', nullable: true })
approvedDate?: Date;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ type: 'text', nullable: true })
body?: string;
@Column({ type: 'text', nullable: true })
remarks?: string;
// --- JSON & Schema Section ---
@Column({ type: 'json', nullable: true })
@@ -87,23 +51,8 @@ export class RfaRevision {
})
vRefDrawingCount?: number;
// --- Timestamp ---
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@Column({ name: 'created_by', nullable: true })
createdBy?: number;
@Column({ name: 'updated_by', nullable: true })
updatedBy?: number;
// --- Relations ---
@ManyToOne(() => Rfa)
@JoinColumn({ name: 'rfa_id' })
rfa!: Rfa;
@ManyToOne(() => RfaStatusCode)
@JoinColumn({ name: 'rfa_status_code_id' })
statusCode!: RfaStatusCode;
@@ -112,10 +61,6 @@ export class RfaRevision {
@JoinColumn({ name: 'rfa_approve_code_id' })
approveCode?: RfaApproveCode;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
creator?: User;
@OneToMany(() => RfaItem, (item) => item.rfaRevision, { cascade: true })
items!: RfaItem[];
@@ -5,14 +5,12 @@ import {
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryColumn,
OneToOne,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { Correspondence } from '../../correspondence/entities/correspondence.entity'; // Import
import { RfaRevision } from './rfa-revision.entity';
import { RfaType } from './rfa-type.entity';
@Entity('rfas')
@@ -45,6 +43,5 @@ export class Rfa {
@JoinColumn({ name: 'created_by' })
creator?: User;
@OneToMany(() => RfaRevision, (revision) => revision.rfa)
revisions!: RfaRevision[];
// Revisions are accessed via correspondence.revisions -> rfaRevision
}
+16 -13
View File
@@ -12,6 +12,7 @@ import { RfaApproveCode } from './entities/rfa-approve-code.entity';
import { RfaRevision } from './entities/rfa-revision.entity';
import { RfaStatusCode } from './entities/rfa-status-code.entity';
import { Rfa } from './entities/rfa.entity';
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
// DTOs
import { WorkflowTransitionDto } from '../workflow-engine/dto/workflow-transition.dto';
@@ -27,6 +28,8 @@ export class RfaWorkflowService {
private readonly rfaRepo: Repository<Rfa>,
@InjectRepository(RfaRevision)
private readonly revisionRepo: Repository<RfaRevision>,
@InjectRepository(CorrespondenceRevision)
private readonly corrRevisionRepo: Repository<CorrespondenceRevision>,
@InjectRepository(RfaStatusCode)
private readonly statusRepo: Repository<RfaStatusCode>,
@InjectRepository(RfaApproveCode)
@@ -44,16 +47,16 @@ export class RfaWorkflowService {
try {
// 1. ดึงข้อมูล Revision ปัจจุบัน
const revision = await this.revisionRepo.findOne({
where: { id: rfaId, isCurrent: true },
const corrRevision = await this.corrRevisionRepo.findOne({
where: { correspondenceId: rfaId, isCurrent: true },
relations: [
'rfa',
'rfa.correspondence',
'rfa.correspondence.discipline',
'rfaRevision',
'correspondence',
'correspondence.discipline',
],
});
if (!revision) {
if (!corrRevision || !corrRevision.rfaRevision) {
throw new NotFoundException(
`Current Revision for RFA ID ${rfaId} not found`
);
@@ -61,8 +64,8 @@ export class RfaWorkflowService {
// 2. สร้าง Context (ข้อมูลประกอบการตัดสินใจ)
const context = {
rfaType: revision.rfa.rfaTypeId,
discipline: revision.rfa.correspondence?.discipline,
rfaType: corrRevision.correspondence?.correspondenceTypeId,
discipline: corrRevision.correspondence?.discipline,
ownerId: userId,
// อาจเพิ่มเงื่อนไขอื่นๆ เช่น จำนวนวัน, ความเร่งด่วน
};
@@ -72,7 +75,7 @@ export class RfaWorkflowService {
const instance = await this.workflowEngine.createInstance(
this.WORKFLOW_CODE,
'rfa_revision',
revision.id.toString(),
corrRevision.id.toString(),
context
);
@@ -87,7 +90,7 @@ export class RfaWorkflowService {
// 5. Sync สถานะกลับตาราง RFA Revision
await this.syncStatus(
revision,
corrRevision.rfaRevision,
transitionResult.nextState,
undefined,
queryRunner
@@ -132,13 +135,13 @@ export class RfaWorkflowService {
// 2. Sync สถานะกลับตารางเดิม
const instance = await this.workflowEngine.getInstanceById(instanceId);
if (instance && instance.entityType === 'rfa_revision') {
const revision = await this.revisionRepo.findOne({
const rfaRev = await this.revisionRepo.findOne({
where: { id: parseInt(instance.entityId) },
});
if (revision) {
if (rfaRev) {
// เช็คว่า Action นี้มีการระบุ Approve Code มาใน Payload หรือไม่ (เช่น '1A', '3R')
const approveCodeStr = dto.payload?.approveCode;
await this.syncStatus(revision, result.nextState, approveCodeStr);
await this.syncStatus(rfaRev, result.nextState, approveCodeStr);
}
}
+4
View File
@@ -5,6 +5,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
// Entities
import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.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 { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity';
import { RoutingTemplate } from '../correspondence/entities/routing-template.entity';
import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity';
@@ -41,6 +43,8 @@ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module'
RfaStatusCode,
RfaApproveCode,
Correspondence,
CorrespondenceRevision,
CorrespondenceStatus,
ShopDrawingRevision,
RfaWorkflow,
RfaWorkflowTemplate,
+101 -40
View File
@@ -14,6 +14,8 @@ import { DataSource, In, Repository } from 'typeorm';
// Entities
import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.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 { RoutingTemplate } from '../correspondence/entities/routing-template.entity';
import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity';
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity';
@@ -54,6 +56,10 @@ export class RfaService {
private correspondenceRepo: Repository<Correspondence>,
@InjectRepository(RfaType)
private rfaTypeRepo: Repository<RfaType>,
@InjectRepository(CorrespondenceRevision)
private corrRevRepo: Repository<CorrespondenceRevision>,
@InjectRepository(CorrespondenceStatus)
private corrStatusRepo: Repository<CorrespondenceStatus>,
@InjectRepository(RfaStatusCode)
private rfaStatusRepo: Repository<RfaStatusCode>,
@InjectRepository(RfaApproveCode)
@@ -120,6 +126,18 @@ export class RfaService {
},
});
// Get Generic Draft Status for Correspondence
const corrStatusDraft = await queryRunner.manager.findOne(
CorrespondenceStatus,
{
where: { statusCode: 'DRAFT' },
}
);
if (!corrStatusDraft)
throw new InternalServerErrorException(
'Correspondence Status DRAFT not found'
);
// 1. Create Correspondence Record
const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber.number,
@@ -134,20 +152,19 @@ export class RfaService {
// 2. Create Rfa Master Record
const rfa = queryRunner.manager.create(Rfa, {
id: savedCorr.id, // ✅ CTI Key share
rfaTypeId: createDto.rfaTypeId,
createdBy: user.user_id,
disciplineId: createDto.disciplineId, // ✅ Add disciplineId
});
const savedRfa = await queryRunner.manager.save(rfa);
// 3. Create First Revision (Draft)
const rfaRevision = queryRunner.manager.create(RfaRevision, {
// 3. Create First Correspondence Revision
const corrRevision = queryRunner.manager.create(CorrespondenceRevision, {
correspondenceId: savedCorr.id,
rfaId: savedRfa.id,
revisionNumber: 0,
revisionLabel: '0',
isCurrent: true,
rfaStatusCodeId: statusDraft.id,
statusId: corrStatusDraft.id,
subject: createDto.subject,
body: createDto.body,
remarks: createDto.remarks,
@@ -157,6 +174,14 @@ export class RfaService {
: new Date(),
dueDate: createDto.dueDate ? new Date(createDto.dueDate) : undefined,
createdBy: user.user_id,
schemaVersion: 1,
});
const savedCorrRev = await queryRunner.manager.save(corrRevision);
// 4. Create First RFA Revision (CTI Extends CorrespondenceRevision)
const rfaRevision = queryRunner.manager.create(RfaRevision, {
id: savedCorrRev.id, // ✅ Matches correspondence revision id
rfaStatusCodeId: statusDraft.id,
details: createDto.details,
schemaVersion: 1,
});
@@ -246,11 +271,12 @@ export class RfaService {
const queryBuilder = this.rfaRepo
.createQueryBuilder('rfa')
.leftJoinAndSelect('rfa.correspondence', 'corr')
.leftJoinAndSelect('rfa.revisions', 'rev')
.leftJoinAndSelect('corr.revisions', 'corrRev')
.leftJoinAndSelect('corrRev.rfaRevision', 'rfaRev')
.leftJoinAndSelect('corr.project', 'project')
.leftJoinAndSelect('corr.discipline', 'discipline')
.leftJoinAndSelect('rev.statusCode', 'status')
.leftJoinAndSelect('rev.items', 'items')
.leftJoinAndSelect('rfaRev.statusCode', 'status')
.leftJoinAndSelect('rfaRev.items', 'items')
.leftJoinAndSelect('items.shopDrawingRevision', 'sdRev')
.leftJoinAndSelect('sdRev.attachments', 'attachments');
@@ -258,9 +284,11 @@ export class RfaService {
const revStatus = query.revisionStatus || 'CURRENT';
if (revStatus === 'CURRENT') {
queryBuilder.where('rev.isCurrent = :isCurrent', { isCurrent: true });
queryBuilder.where('corrRev.isCurrent = :isCurrent', { isCurrent: true });
} else if (revStatus === 'OLD') {
queryBuilder.where('rev.isCurrent = :isCurrent', { isCurrent: false });
queryBuilder.where('corrRev.isCurrent = :isCurrent', {
isCurrent: false,
});
}
// If 'ALL', no filter
@@ -274,7 +302,7 @@ export class RfaService {
if (search) {
queryBuilder.andWhere(
'(corr.correspondenceNumber LIKE :search OR rev.subject LIKE :search)',
'(corr.correspondenceNumber LIKE :search OR corrRev.subject LIKE :search)',
{ search: `%${search}%` }
);
}
@@ -289,8 +317,20 @@ export class RfaService {
`[DEBUG] RFA findAll: Found ${total} items. Query: ${JSON.stringify(query)}`
);
// Map `revisions` property back to the expected payload for the frontend
const mappedItems = items.map((rfa) => {
const mappedRfa = { ...rfa } as any;
mappedRfa.revisions =
rfa.correspondence?.revisions?.map((cr) => ({
...cr,
...(cr.rfaRevision || {}),
id: cr.rfaRevision?.id || cr.id,
})) || [];
return mappedRfa;
});
return {
data: items,
data: mappedItems,
meta: {
total,
page,
@@ -300,21 +340,22 @@ export class RfaService {
};
}
async findOne(id: number) {
async findOne(id: number, rawEntities = false) {
const rfa = await this.rfaRepo.findOne({
where: { id },
relations: [
'correspondence', // ✅ Add relation to master correspondence
'correspondence',
'rfaType',
'revisions',
'revisions.statusCode',
'revisions.approveCode',
'revisions.items',
'revisions.items.shopDrawingRevision',
'revisions.items.shopDrawingRevision.shopDrawing',
'correspondence.revisions',
'correspondence.revisions.rfaRevision',
'correspondence.revisions.rfaRevision.statusCode',
'correspondence.revisions.rfaRevision.approveCode',
'correspondence.revisions.rfaRevision.items',
'correspondence.revisions.rfaRevision.items.shopDrawingRevision',
'correspondence.revisions.rfaRevision.items.shopDrawingRevision.shopDrawing',
],
order: {
revisions: { revisionNumber: 'DESC' },
correspondence: { revisions: { revisionNumber: 'DESC' } },
},
});
@@ -322,16 +363,33 @@ export class RfaService {
throw new NotFoundException(`RFA ID ${id} not found`);
}
return rfa;
if (rawEntities) {
return rfa;
}
// Map to structure expected by frontend DTO
const mappedRfa = { ...rfa } as any;
mappedRfa.revisions =
rfa.correspondence?.revisions?.map((cr) => ({
...cr,
...(cr.rfaRevision || {}),
id: cr.rfaRevision?.id || cr.id,
})) || [];
return mappedRfa;
}
async submit(rfaId: number, templateId: number, user: User) {
const rfa = await this.findOne(rfaId);
const currentRevision = rfa.revisions.find((r) => r.isCurrent);
if (!currentRevision)
const rfa = await this.findOne(rfaId, true);
const currentCorrRev = rfa.correspondence?.revisions?.find(
(r: any) => r.isCurrent
);
if (!currentCorrRev || !currentCorrRev.rfaRevision)
throw new NotFoundException('Current revision not found');
if (currentRevision.statusCode.statusCode !== 'DFT') {
const currentRfaRev = currentCorrRev.rfaRevision;
if (currentRfaRev.statusCode.statusCode !== 'DFT') {
throw new BadRequestException('Only DRAFT documents can be submitted');
}
@@ -366,9 +424,10 @@ export class RfaService {
try {
// Update Revision Status
currentRevision.rfaStatusCodeId = statusForApprove.id;
currentRevision.issuedDate = new Date();
await queryRunner.manager.save(currentRevision);
currentRfaRev.rfaStatusCodeId = statusForApprove.id;
currentCorrRev.issuedDate = new Date();
await queryRunner.manager.save(currentRfaRev);
await queryRunner.manager.save(currentCorrRev);
// Create First Routing Step
const firstStep = steps[0];
@@ -395,7 +454,7 @@ export class RfaService {
if (recipientUserId) {
await this.notificationService.send({
userId: recipientUserId,
title: `RFA Submitted: ${currentRevision.subject}`,
title: `RFA Submitted: ${currentCorrRev.subject}`,
message: `RFA ${rfa.correspondence.correspondenceNumber} submitted for approval.`,
type: 'SYSTEM',
entityType: 'rfa',
@@ -415,13 +474,15 @@ export class RfaService {
async processAction(rfaId: number, dto: WorkflowActionDto, user: User) {
// Logic คงเดิม: หา Current Routing -> Check Permission -> Call Workflow Engine -> Update DB
// ใช้ this.workflowEngine.processAction (Legacy Support)
// ... (สามารถใช้ Code เดิมจากที่คุณแนบมาได้เลย เพราะ Logic ถูกต้องแล้วสำหรับการใช้ CorrespondenceRouting) ...
const rfa = await this.findOne(rfaId);
const currentRevision = rfa.revisions.find((r) => r.isCurrent);
if (!currentRevision)
const rfa = await this.findOne(rfaId, true);
const currentCorrRev = rfa.correspondence?.revisions?.find(
(r: any) => r.isCurrent
);
if (!currentCorrRev || !currentCorrRev.rfaRevision)
throw new NotFoundException('Current revision not found');
const currentRfaRev = currentCorrRev.rfaRevision;
const currentRouting = await this.routingRepo.findOne({
where: {
correspondenceId: rfa.correspondence.id, // ✅ Use master correspondence id
@@ -509,16 +570,16 @@ export class RfaService {
},
}); // Logic Map Code อย่างง่าย
if (approveCode) {
currentRevision.rfaApproveCodeId = approveCode.id;
currentRevision.approvedDate = new Date();
currentRfaRev.rfaApproveCodeId = approveCode.id;
currentRfaRev.approvedDate = new Date();
}
} else {
const rejectCode = await this.rfaApproveRepo.findOne({
where: { approveCode: '4X' },
});
if (rejectCode) currentRevision.rfaApproveCodeId = rejectCode.id;
if (rejectCode) currentRfaRev.rfaApproveCodeId = rejectCode.id;
}
await queryRunner.manager.save(currentRevision);
await queryRunner.manager.save(currentRfaRev);
}
await queryRunner.commitTransaction();