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, JoinColumn,
CreateDateColumn, CreateDateColumn,
Index, Index,
OneToOne,
} from 'typeorm'; } from 'typeorm';
import { RfaRevision } from '../../rfa/entities/rfa-revision.entity';
import { Correspondence } from './correspondence.entity'; import { Correspondence } from './correspondence.entity';
import { CorrespondenceStatus } from './correspondence-status.entity'; import { CorrespondenceStatus } from './correspondence-status.entity';
import { User } from '../../user/entities/user.entity'; import { User } from '../../user/entities/user.entity';
@@ -107,4 +109,8 @@ export class CorrespondenceRevision {
@ManyToOne(() => User) @ManyToOne(() => User)
@JoinColumn({ name: 'created_by' }) @JoinColumn({ name: 'created_by' })
creator?: User; 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() @IsOptional()
received_date?: string; received_date?: string;
@IsNumber()
@IsOptional()
discipline_id?: number;
@IsString() @IsString()
@IsOptional() @IsOptional()
body?: string; body?: string;
@@ -127,22 +127,25 @@ export class MigrationService {
correspondenceNumber: dto.document_number, correspondenceNumber: dto.document_number,
correspondenceTypeId: typeId, correspondenceTypeId: typeId,
projectId: project.id, projectId: project.id,
disciplineId: dto.discipline_id || undefined,
isInternal: false, isInternal: false,
createdBy: userId, createdBy: userId,
}); });
await queryRunner.manager.save(correspondence); 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 // 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; let attachmentId: number | null = null;
if (dto.source_file_path) { if (dto.source_file_path) {
try { try {
const attachment = await this.fileStorageService.importStagingFile( const attachment = await this.fileStorageService.importStagingFile(
dto.source_file_path, dto.source_file_path,
userId, userId,
{ documentType: dto.category } // use category from DTO directly { documentType: dto.category }
); );
attachmentId = attachment.id; attachmentId = attachment.id;
} catch (fileError: unknown) { } 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 revNum = revisionCount;
const revision = queryRunner.manager.create(CorrespondenceRevision, { const revision = queryRunner.manager.create(CorrespondenceRevision, {
correspondenceId: correspondence.id, correspondenceId: correspondence.id,
@@ -173,15 +175,16 @@ export class MigrationService {
statusId: status.id, statusId: status.id,
subject: dto.title, subject: dto.title,
description: 'Migrated from legacy system via Auto Ingest', description: 'Migrated from legacy system via Auto Ingest',
body: dto.body || undefined, // Map from DTO
details: { details: {
...dto.details, ...dto.details,
ai_confidence: dto.ai_confidence, ai_confidence: dto.ai_confidence,
ai_issues: dto.ai_issues as unknown, ai_issues: dto.ai_issues as unknown,
source_file_path: dto.source_file_path, source_file_path: dto.source_file_path,
attachment_id: attachmentId, // Link attachment ID if successful attachment_id: attachmentId,
}, },
schemaVersion: 1, schemaVersion: 1,
createdBy: userId, // Bot ID createdBy: userId,
}); });
if (revisionCount > 0) { if (revisionCount > 0) {
@@ -1,39 +1,27 @@
// File: src/modules/rfa/entities/rfa-revision.entity.ts // File: src/modules/rfa/entities/rfa-revision.entity.ts
import { import {
Column, Column,
CreateDateColumn,
Entity, Entity,
JoinColumn, JoinColumn,
ManyToOne, ManyToOne,
OneToMany, OneToMany,
PrimaryGeneratedColumn, PrimaryColumn,
Unique, OneToOne,
} from 'typeorm'; } 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 { RfaApproveCode } from './rfa-approve-code.entity';
import { RfaItem } from './rfa-item.entity'; import { RfaItem } from './rfa-item.entity';
import { RfaStatusCode } from './rfa-status-code.entity'; import { RfaStatusCode } from './rfa-status-code.entity';
import { RfaWorkflow } from './rfa-workflow.entity'; import { RfaWorkflow } from './rfa-workflow.entity';
import { Rfa } from './rfa.entity';
@Entity('rfa_revisions') @Entity('rfa_revisions')
@Unique(['rfaId', 'revisionNumber'])
@Unique(['rfaId', 'isCurrent'])
export class RfaRevision { export class RfaRevision {
@PrimaryGeneratedColumn() @PrimaryColumn()
id!: number; id!: number;
@Column({ name: 'rfa_id' }) @OneToOne(() => CorrespondenceRevision, { onDelete: 'CASCADE' })
rfaId!: number; @JoinColumn({ name: 'id' })
correspondenceRevision!: CorrespondenceRevision;
@Column({ name: 'revision_number' })
revisionNumber!: number;
@Column({ name: 'revision_label', length: 10, nullable: true })
revisionLabel?: string;
@Column({ name: 'is_current', default: false })
isCurrent!: boolean;
@Column({ name: 'rfa_status_code_id' }) @Column({ name: 'rfa_status_code_id' })
rfaStatusCodeId!: number; rfaStatusCodeId!: number;
@@ -41,33 +29,9 @@ export class RfaRevision {
@Column({ name: 'rfa_approve_code_id', nullable: true }) @Column({ name: 'rfa_approve_code_id', nullable: true })
rfaApproveCodeId?: number; 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 }) @Column({ name: 'approved_date', type: 'date', nullable: true })
approvedDate?: Date; 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 --- // --- JSON & Schema Section ---
@Column({ type: 'json', nullable: true }) @Column({ type: 'json', nullable: true })
@@ -87,23 +51,8 @@ export class RfaRevision {
}) })
vRefDrawingCount?: number; 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 --- // --- Relations ---
@ManyToOne(() => Rfa)
@JoinColumn({ name: 'rfa_id' })
rfa!: Rfa;
@ManyToOne(() => RfaStatusCode) @ManyToOne(() => RfaStatusCode)
@JoinColumn({ name: 'rfa_status_code_id' }) @JoinColumn({ name: 'rfa_status_code_id' })
statusCode!: RfaStatusCode; statusCode!: RfaStatusCode;
@@ -112,10 +61,6 @@ export class RfaRevision {
@JoinColumn({ name: 'rfa_approve_code_id' }) @JoinColumn({ name: 'rfa_approve_code_id' })
approveCode?: RfaApproveCode; approveCode?: RfaApproveCode;
@ManyToOne(() => User)
@JoinColumn({ name: 'created_by' })
creator?: User;
@OneToMany(() => RfaItem, (item) => item.rfaRevision, { cascade: true }) @OneToMany(() => RfaItem, (item) => item.rfaRevision, { cascade: true })
items!: RfaItem[]; items!: RfaItem[];
@@ -5,14 +5,12 @@ import {
Entity, Entity,
JoinColumn, JoinColumn,
ManyToOne, ManyToOne,
OneToMany,
PrimaryColumn, PrimaryColumn,
OneToOne, OneToOne,
} from 'typeorm'; } from 'typeorm';
import { User } from '../../user/entities/user.entity'; import { User } from '../../user/entities/user.entity';
import { Correspondence } from '../../correspondence/entities/correspondence.entity'; // Import import { Correspondence } from '../../correspondence/entities/correspondence.entity'; // Import
import { RfaRevision } from './rfa-revision.entity';
import { RfaType } from './rfa-type.entity'; import { RfaType } from './rfa-type.entity';
@Entity('rfas') @Entity('rfas')
@@ -45,6 +43,5 @@ export class Rfa {
@JoinColumn({ name: 'created_by' }) @JoinColumn({ name: 'created_by' })
creator?: User; creator?: User;
@OneToMany(() => RfaRevision, (revision) => revision.rfa) // Revisions are accessed via correspondence.revisions -> rfaRevision
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 { RfaRevision } from './entities/rfa-revision.entity';
import { RfaStatusCode } from './entities/rfa-status-code.entity'; import { RfaStatusCode } from './entities/rfa-status-code.entity';
import { Rfa } from './entities/rfa.entity'; import { Rfa } from './entities/rfa.entity';
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
// DTOs // DTOs
import { WorkflowTransitionDto } from '../workflow-engine/dto/workflow-transition.dto'; import { WorkflowTransitionDto } from '../workflow-engine/dto/workflow-transition.dto';
@@ -27,6 +28,8 @@ export class RfaWorkflowService {
private readonly rfaRepo: Repository<Rfa>, private readonly rfaRepo: Repository<Rfa>,
@InjectRepository(RfaRevision) @InjectRepository(RfaRevision)
private readonly revisionRepo: Repository<RfaRevision>, private readonly revisionRepo: Repository<RfaRevision>,
@InjectRepository(CorrespondenceRevision)
private readonly corrRevisionRepo: Repository<CorrespondenceRevision>,
@InjectRepository(RfaStatusCode) @InjectRepository(RfaStatusCode)
private readonly statusRepo: Repository<RfaStatusCode>, private readonly statusRepo: Repository<RfaStatusCode>,
@InjectRepository(RfaApproveCode) @InjectRepository(RfaApproveCode)
@@ -44,16 +47,16 @@ export class RfaWorkflowService {
try { try {
// 1. ดึงข้อมูล Revision ปัจจุบัน // 1. ดึงข้อมูล Revision ปัจจุบัน
const revision = await this.revisionRepo.findOne({ const corrRevision = await this.corrRevisionRepo.findOne({
where: { id: rfaId, isCurrent: true }, where: { correspondenceId: rfaId, isCurrent: true },
relations: [ relations: [
'rfa', 'rfaRevision',
'rfa.correspondence', 'correspondence',
'rfa.correspondence.discipline', 'correspondence.discipline',
], ],
}); });
if (!revision) { if (!corrRevision || !corrRevision.rfaRevision) {
throw new NotFoundException( throw new NotFoundException(
`Current Revision for RFA ID ${rfaId} not found` `Current Revision for RFA ID ${rfaId} not found`
); );
@@ -61,8 +64,8 @@ export class RfaWorkflowService {
// 2. สร้าง Context (ข้อมูลประกอบการตัดสินใจ) // 2. สร้าง Context (ข้อมูลประกอบการตัดสินใจ)
const context = { const context = {
rfaType: revision.rfa.rfaTypeId, rfaType: corrRevision.correspondence?.correspondenceTypeId,
discipline: revision.rfa.correspondence?.discipline, discipline: corrRevision.correspondence?.discipline,
ownerId: userId, ownerId: userId,
// อาจเพิ่มเงื่อนไขอื่นๆ เช่น จำนวนวัน, ความเร่งด่วน // อาจเพิ่มเงื่อนไขอื่นๆ เช่น จำนวนวัน, ความเร่งด่วน
}; };
@@ -72,7 +75,7 @@ export class RfaWorkflowService {
const instance = await this.workflowEngine.createInstance( const instance = await this.workflowEngine.createInstance(
this.WORKFLOW_CODE, this.WORKFLOW_CODE,
'rfa_revision', 'rfa_revision',
revision.id.toString(), corrRevision.id.toString(),
context context
); );
@@ -87,7 +90,7 @@ export class RfaWorkflowService {
// 5. Sync สถานะกลับตาราง RFA Revision // 5. Sync สถานะกลับตาราง RFA Revision
await this.syncStatus( await this.syncStatus(
revision, corrRevision.rfaRevision,
transitionResult.nextState, transitionResult.nextState,
undefined, undefined,
queryRunner queryRunner
@@ -132,13 +135,13 @@ export class RfaWorkflowService {
// 2. Sync สถานะกลับตารางเดิม // 2. Sync สถานะกลับตารางเดิม
const instance = await this.workflowEngine.getInstanceById(instanceId); const instance = await this.workflowEngine.getInstanceById(instanceId);
if (instance && instance.entityType === 'rfa_revision') { if (instance && instance.entityType === 'rfa_revision') {
const revision = await this.revisionRepo.findOne({ const rfaRev = await this.revisionRepo.findOne({
where: { id: parseInt(instance.entityId) }, where: { id: parseInt(instance.entityId) },
}); });
if (revision) { if (rfaRev) {
// เช็คว่า Action นี้มีการระบุ Approve Code มาใน Payload หรือไม่ (เช่น '1A', '3R') // เช็คว่า Action นี้มีการระบุ Approve Code มาใน Payload หรือไม่ (เช่น '1A', '3R')
const approveCodeStr = dto.payload?.approveCode; 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 // Entities
import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity'; import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity';
import { Correspondence } from '../correspondence/entities/correspondence.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 { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity';
import { RoutingTemplate } from '../correspondence/entities/routing-template.entity'; import { RoutingTemplate } from '../correspondence/entities/routing-template.entity';
import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity'; import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity';
@@ -41,6 +43,8 @@ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module'
RfaStatusCode, RfaStatusCode,
RfaApproveCode, RfaApproveCode,
Correspondence, Correspondence,
CorrespondenceRevision,
CorrespondenceStatus,
ShopDrawingRevision, ShopDrawingRevision,
RfaWorkflow, RfaWorkflow,
RfaWorkflowTemplate, RfaWorkflowTemplate,
+100 -39
View File
@@ -14,6 +14,8 @@ import { DataSource, In, Repository } from 'typeorm';
// Entities // Entities
import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity'; import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity';
import { Correspondence } from '../correspondence/entities/correspondence.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 { RoutingTemplate } from '../correspondence/entities/routing-template.entity';
import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity'; import { RoutingTemplateStep } from '../correspondence/entities/routing-template-step.entity';
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity'; import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity';
@@ -54,6 +56,10 @@ export class RfaService {
private correspondenceRepo: Repository<Correspondence>, private correspondenceRepo: Repository<Correspondence>,
@InjectRepository(RfaType) @InjectRepository(RfaType)
private rfaTypeRepo: Repository<RfaType>, private rfaTypeRepo: Repository<RfaType>,
@InjectRepository(CorrespondenceRevision)
private corrRevRepo: Repository<CorrespondenceRevision>,
@InjectRepository(CorrespondenceStatus)
private corrStatusRepo: Repository<CorrespondenceStatus>,
@InjectRepository(RfaStatusCode) @InjectRepository(RfaStatusCode)
private rfaStatusRepo: Repository<RfaStatusCode>, private rfaStatusRepo: Repository<RfaStatusCode>,
@InjectRepository(RfaApproveCode) @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 // 1. Create Correspondence Record
const correspondence = queryRunner.manager.create(Correspondence, { const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber.number, correspondenceNumber: docNumber.number,
@@ -134,20 +152,19 @@ export class RfaService {
// 2. Create Rfa Master Record // 2. Create Rfa Master Record
const rfa = queryRunner.manager.create(Rfa, { const rfa = queryRunner.manager.create(Rfa, {
id: savedCorr.id, // ✅ CTI Key share
rfaTypeId: createDto.rfaTypeId, rfaTypeId: createDto.rfaTypeId,
createdBy: user.user_id, createdBy: user.user_id,
disciplineId: createDto.disciplineId, // ✅ Add disciplineId
}); });
const savedRfa = await queryRunner.manager.save(rfa); const savedRfa = await queryRunner.manager.save(rfa);
// 3. Create First Revision (Draft) // 3. Create First Correspondence Revision
const rfaRevision = queryRunner.manager.create(RfaRevision, { const corrRevision = queryRunner.manager.create(CorrespondenceRevision, {
correspondenceId: savedCorr.id, correspondenceId: savedCorr.id,
rfaId: savedRfa.id,
revisionNumber: 0, revisionNumber: 0,
revisionLabel: '0', revisionLabel: '0',
isCurrent: true, isCurrent: true,
rfaStatusCodeId: statusDraft.id, statusId: corrStatusDraft.id,
subject: createDto.subject, subject: createDto.subject,
body: createDto.body, body: createDto.body,
remarks: createDto.remarks, remarks: createDto.remarks,
@@ -157,6 +174,14 @@ export class RfaService {
: new Date(), : new Date(),
dueDate: createDto.dueDate ? new Date(createDto.dueDate) : undefined, dueDate: createDto.dueDate ? new Date(createDto.dueDate) : undefined,
createdBy: user.user_id, 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, details: createDto.details,
schemaVersion: 1, schemaVersion: 1,
}); });
@@ -246,11 +271,12 @@ export class RfaService {
const queryBuilder = this.rfaRepo const queryBuilder = this.rfaRepo
.createQueryBuilder('rfa') .createQueryBuilder('rfa')
.leftJoinAndSelect('rfa.correspondence', 'corr') .leftJoinAndSelect('rfa.correspondence', 'corr')
.leftJoinAndSelect('rfa.revisions', 'rev') .leftJoinAndSelect('corr.revisions', 'corrRev')
.leftJoinAndSelect('corrRev.rfaRevision', 'rfaRev')
.leftJoinAndSelect('corr.project', 'project') .leftJoinAndSelect('corr.project', 'project')
.leftJoinAndSelect('corr.discipline', 'discipline') .leftJoinAndSelect('corr.discipline', 'discipline')
.leftJoinAndSelect('rev.statusCode', 'status') .leftJoinAndSelect('rfaRev.statusCode', 'status')
.leftJoinAndSelect('rev.items', 'items') .leftJoinAndSelect('rfaRev.items', 'items')
.leftJoinAndSelect('items.shopDrawingRevision', 'sdRev') .leftJoinAndSelect('items.shopDrawingRevision', 'sdRev')
.leftJoinAndSelect('sdRev.attachments', 'attachments'); .leftJoinAndSelect('sdRev.attachments', 'attachments');
@@ -258,9 +284,11 @@ export class RfaService {
const revStatus = query.revisionStatus || 'CURRENT'; const revStatus = query.revisionStatus || 'CURRENT';
if (revStatus === 'CURRENT') { if (revStatus === 'CURRENT') {
queryBuilder.where('rev.isCurrent = :isCurrent', { isCurrent: true }); queryBuilder.where('corrRev.isCurrent = :isCurrent', { isCurrent: true });
} else if (revStatus === 'OLD') { } else if (revStatus === 'OLD') {
queryBuilder.where('rev.isCurrent = :isCurrent', { isCurrent: false }); queryBuilder.where('corrRev.isCurrent = :isCurrent', {
isCurrent: false,
});
} }
// If 'ALL', no filter // If 'ALL', no filter
@@ -274,7 +302,7 @@ export class RfaService {
if (search) { if (search) {
queryBuilder.andWhere( queryBuilder.andWhere(
'(corr.correspondenceNumber LIKE :search OR rev.subject LIKE :search)', '(corr.correspondenceNumber LIKE :search OR corrRev.subject LIKE :search)',
{ search: `%${search}%` } { search: `%${search}%` }
); );
} }
@@ -289,8 +317,20 @@ export class RfaService {
`[DEBUG] RFA findAll: Found ${total} items. Query: ${JSON.stringify(query)}` `[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 { return {
data: items, data: mappedItems,
meta: { meta: {
total, total,
page, page,
@@ -300,21 +340,22 @@ export class RfaService {
}; };
} }
async findOne(id: number) { async findOne(id: number, rawEntities = false) {
const rfa = await this.rfaRepo.findOne({ const rfa = await this.rfaRepo.findOne({
where: { id }, where: { id },
relations: [ relations: [
'correspondence', // ✅ Add relation to master correspondence 'correspondence',
'rfaType', 'rfaType',
'revisions', 'correspondence.revisions',
'revisions.statusCode', 'correspondence.revisions.rfaRevision',
'revisions.approveCode', 'correspondence.revisions.rfaRevision.statusCode',
'revisions.items', 'correspondence.revisions.rfaRevision.approveCode',
'revisions.items.shopDrawingRevision', 'correspondence.revisions.rfaRevision.items',
'revisions.items.shopDrawingRevision.shopDrawing', 'correspondence.revisions.rfaRevision.items.shopDrawingRevision',
'correspondence.revisions.rfaRevision.items.shopDrawingRevision.shopDrawing',
], ],
order: { order: {
revisions: { revisionNumber: 'DESC' }, correspondence: { revisions: { revisionNumber: 'DESC' } },
}, },
}); });
@@ -322,16 +363,33 @@ export class RfaService {
throw new NotFoundException(`RFA ID ${id} not found`); throw new NotFoundException(`RFA ID ${id} not found`);
} }
if (rawEntities) {
return rfa; return rfa;
} }
async submit(rfaId: number, templateId: number, user: User) { // Map to structure expected by frontend DTO
const rfa = await this.findOne(rfaId); const mappedRfa = { ...rfa } as any;
const currentRevision = rfa.revisions.find((r) => r.isCurrent); mappedRfa.revisions =
rfa.correspondence?.revisions?.map((cr) => ({
...cr,
...(cr.rfaRevision || {}),
id: cr.rfaRevision?.id || cr.id,
})) || [];
if (!currentRevision) return mappedRfa;
}
async submit(rfaId: number, templateId: number, user: User) {
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'); 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'); throw new BadRequestException('Only DRAFT documents can be submitted');
} }
@@ -366,9 +424,10 @@ export class RfaService {
try { try {
// Update Revision Status // Update Revision Status
currentRevision.rfaStatusCodeId = statusForApprove.id; currentRfaRev.rfaStatusCodeId = statusForApprove.id;
currentRevision.issuedDate = new Date(); currentCorrRev.issuedDate = new Date();
await queryRunner.manager.save(currentRevision); await queryRunner.manager.save(currentRfaRev);
await queryRunner.manager.save(currentCorrRev);
// Create First Routing Step // Create First Routing Step
const firstStep = steps[0]; const firstStep = steps[0];
@@ -395,7 +454,7 @@ export class RfaService {
if (recipientUserId) { if (recipientUserId) {
await this.notificationService.send({ await this.notificationService.send({
userId: recipientUserId, userId: recipientUserId,
title: `RFA Submitted: ${currentRevision.subject}`, title: `RFA Submitted: ${currentCorrRev.subject}`,
message: `RFA ${rfa.correspondence.correspondenceNumber} submitted for approval.`, message: `RFA ${rfa.correspondence.correspondenceNumber} submitted for approval.`,
type: 'SYSTEM', type: 'SYSTEM',
entityType: 'rfa', entityType: 'rfa',
@@ -415,13 +474,15 @@ export class RfaService {
async processAction(rfaId: number, dto: WorkflowActionDto, user: User) { async processAction(rfaId: number, dto: WorkflowActionDto, user: User) {
// Logic คงเดิม: หา Current Routing -> Check Permission -> Call Workflow Engine -> Update DB // Logic คงเดิม: หา Current Routing -> Check Permission -> Call Workflow Engine -> Update DB
// ใช้ this.workflowEngine.processAction (Legacy Support) const rfa = await this.findOne(rfaId, true);
// ... (สามารถใช้ Code เดิมจากที่คุณแนบมาได้เลย เพราะ Logic ถูกต้องแล้วสำหรับการใช้ CorrespondenceRouting) ... const currentCorrRev = rfa.correspondence?.revisions?.find(
const rfa = await this.findOne(rfaId); (r: any) => r.isCurrent
const currentRevision = rfa.revisions.find((r) => r.isCurrent); );
if (!currentRevision) if (!currentCorrRev || !currentCorrRev.rfaRevision)
throw new NotFoundException('Current revision not found'); throw new NotFoundException('Current revision not found');
const currentRfaRev = currentCorrRev.rfaRevision;
const currentRouting = await this.routingRepo.findOne({ const currentRouting = await this.routingRepo.findOne({
where: { where: {
correspondenceId: rfa.correspondence.id, // ✅ Use master correspondence id correspondenceId: rfa.correspondence.id, // ✅ Use master correspondence id
@@ -509,16 +570,16 @@ export class RfaService {
}, },
}); // Logic Map Code อย่างง่าย }); // Logic Map Code อย่างง่าย
if (approveCode) { if (approveCode) {
currentRevision.rfaApproveCodeId = approveCode.id; currentRfaRev.rfaApproveCodeId = approveCode.id;
currentRevision.approvedDate = new Date(); currentRfaRev.approvedDate = new Date();
} }
} else { } else {
const rejectCode = await this.rfaApproveRepo.findOne({ const rejectCode = await this.rfaApproveRepo.findOne({
where: { approveCode: '4X' }, 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(); await queryRunner.commitTransaction();
+45
View File
@@ -0,0 +1,45 @@
const fs = require('fs');
const config = $('Set Configuration').first().json.config;
// Check file mount and inputs
try {
if (!fs.existsSync(config.EXCEL_FILE)) {
throw new Error(`Excel file not found at: ${config.EXCEL_FILE}`);
}
if (!fs.existsSync(config.SOURCE_PDF_DIR)) {
throw new Error(`PDF Source directory not found at: ${config.SOURCE_PDF_DIR}`);
}
const files = fs.readdirSync(config.SOURCE_PDF_DIR);
// Check write permission to log path
fs.writeFileSync(`${config.LOG_PATH}/.preflight_ok`, new Date().toISOString());
// Grab categories out of the previous node (Fetch Categories) if available
// otherwise use fallback array
let categories = ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];
try {
const upstreamData = $('Fetch Categories').first()?.json?.data;
if (upstreamData && Array.isArray(upstreamData)) {
categories = upstreamData.map(c => c.name || c.type || c); // very loose mapping depending on API response
}
} catch(e) {}
// Grab existing tags from Fetch Tags node
let existingTags = [];
try {
const tagData = $('Fetch Tags').first()?.json?.data || [];
existingTags = Array.isArray(tagData) ? tagData.map(t => t.tag_name || t.name || '').filter(Boolean) : [];
} catch(e) {}
return [{ json: {
preflight_ok: true,
pdf_count_in_source: files.length,
excel_target: config.EXCEL_FILE,
system_categories: categories,
existing_tags: existingTags,
timestamp: new Date().toISOString()
}}];
} catch (err) {
throw new Error(`Pre-flight check failed: ${err.message}`);
}
@@ -623,24 +623,10 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
**Purpose**: Child table storing revision history of RFAs (1:N) **Purpose**: Child table storing revision history of RFAs (1:N)
| Column Name | Data Type | Constraints | Description | | Column Name | Data Type | Constraints | Description |
| ----------- | --------- | --------------------------- | ------------------ | | ------------------- | --------- | --------------------------------- | ----------------------------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | | id | INT | PK, FK | Master Revision ID (Shared with correspondence_revisions) |
| rfa_id | INT | NOT NULL, FK | Master RFA ID |
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) |
| is_current | BOOLEAN | DEFAULT FALSE | Current revision flag |
| rfa_status_code_id | INT | NOT NULL, FK | Current RFA status | | rfa_status_code_id | INT | NOT NULL, FK | Current RFA status |
| rfa_approve_code_id | INT | NULL, FK | Approval result code | | rfa_approve_code_id | INT | NULL, FK | Approval result code |
| title | VARCHAR(255) | NOT NULL | RFA title |
| document_date | DATE | NULL | Document date |
| issued_date | DATE | NULL | Issue date for approval |
| received_date | DATETIME | NULL | Received date |
| approved_date | DATE | NULL | Approval date |
| description | TEXT | NULL | Revision description |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp |
| created_by | INT | NULL, FK | User who created revision |
| updated_by | INT | NULL, FK | User who last updated |
| details | JSON | NULL | Type-specific details (e.g., RFI questions) | | details | JSON | NULL | Type-specific details (e.g., RFI questions) |
| v_ref_drawing_count | INT | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Drawing Count จาก JSON details เพื่อทำ Index | | v_ref_drawing_count | INT | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Drawing Count จาก JSON details เพื่อทำ Index |
| schema_version | INT | DEFAULT 1 | Version of the schema used with this details | | schema_version | INT | DEFAULT 1 | Version of the schema used with this details |
@@ -648,21 +634,16 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
**Indexes**: **Indexes**:
* PRIMARY KEY (id) * PRIMARY KEY (id)
* FOREIGN KEY (rfa_id) REFERENCES rfas(id) ON DELETE CASCADE * FOREIGN KEY (id) REFERENCES correspondence_revisions(id) ON DELETE CASCADE
* FOREIGN KEY (rfa_status_code_id) REFERENCES rfa_status_codes(id) * FOREIGN KEY (rfa_status_code_id) REFERENCES rfa_status_codes(id)
* FOREIGN KEY (rfa_approve_code_id) REFERENCES rfa_approve_codes(id) ON DELETE SET NULL * FOREIGN KEY (rfa_approve_code_id) REFERENCES rfa_approve_codes(id) ON DELETE SET NULL
* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL
* FOREIGN KEY (updated_by) REFERENCES users(user_id) ON DELETE SET NULL
* UNIQUE KEY (rfa_id, revision_number)
* UNIQUE KEY (rfa_id, is_current)
* INDEX (rfa_status_code_id) * INDEX (rfa_status_code_id)
* INDEX (rfa_approve_code_id) * INDEX (rfa_approve_code_id)
* INDEX (is_current)
* INDEX (v_ref_drawing_count): ตัวอย่างการ Index ข้อมูลตัวเลขใน JSON * INDEX (v_ref_drawing_count): ตัวอย่างการ Index ข้อมูลตัวเลขใน JSON
**Relationships**: **Relationships**:
* Parent: correspondences, rfas, rfa_status_codes, rfa_approve_codes, users * Parent: correspondence_revisions, rfas, rfa_status_codes, rfa_approve_codes
* Children: rfa_items * Children: rfa_items
--- ---
+142
View File
@@ -0,0 +1,142 @@
const config = $('Set Configuration').first().json.config;
const fallbackState = $('Check Fallback State').first()?.json || { is_fallback_active: false, recent_error_count: 0 };
const isFallback = fallbackState.is_fallback_active || false;
const model = isFallback ? config.OLLAMA_MODEL_FALLBACK : config.OLLAMA_MODEL_PRIMARY;
// Read DB Context
const dbContext = $('Fetch DB Context').all().map(i => i.json);
const dbProjects = dbContext.filter(d => d.type === 'projects').map(d => ({id: d.id, code: d.text1, name: d.text2}));
const dbDisciplines = dbContext.filter(d => d.type === 'disciplines').map(d => ({id: d.id, th: d.text1, en: d.text2}));
const dbOrgs = dbContext.filter(d => d.type === 'organizations').map(d => ({id: d.id, name: d.text1, code: d.text2}));
const dbTags = dbContext.filter(d => d.type === 'tags').map(d => ({id: d.id, name: d.text1}));
const dbCorrTypes = dbContext.filter(d => d.type === 'correspondence_types').map(d => ({id: d.id, code: d.text1, name: d.text2}));
let systemCategories = ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];
try { systemCategories = $('File Mount Check').first().json.system_categories || systemCategories; } catch (e) {}
const pdfItems = $('Extract PDF Text').all();
// File Validator passes all original Excel JSON fields through (sender, receiver, project_code, etc.)
// Read PDF File overwrites the JSON with binary data, so we must go back one step
const metaItems = $('File Validator').all();
return pdfItems.map((pdfItem, i) => {
const item = metaItems[i] || pdfItem;
const docNum = String(item.json.document_number || '');
const title = String(item.json.title || '');
const legacyNum = String(item.json.legacy_number || '');
const issuedDate = String(item.json.issued_date || '');
const receivedDate = String(item.json.received_date || '');
const corrType = String(item.json.correspondence_type || '');
const senderCode = String(item.json.sender || '');
const receiverCode = String(item.json.receiver || '');
const projectCode = String(item.json.project_code || '');
// JavaScript pre-mapping
const findOrgId = (code) => {
if (!code) return null;
const match = dbOrgs.find(o => o.code === code || o.name === code);
return match ? match.id : null;
};
const findProjectId = (code) => {
if (!code) return config.PROJECT_ID; // Fallback to config
const match = dbProjects.find(p => p.code === code || p.name === code);
return match ? match.id : config.PROJECT_ID;
};
const senderId = findOrgId(senderCode);
const receiverId = findOrgId(receiverCode);
const projectId = findProjectId(projectCode);
// Excel corrType is likely already the ID based on requirements, but fallback matching to ID if needed
const corrMatch = dbCorrTypes.find(c => String(c.id) === corrType || c.code === corrType || c.name === corrType);
const corrTypeId = corrMatch ? corrMatch.id : (isNaN(parseInt(corrType)) ? null : parseInt(corrType));
const isRFA = docNum.includes('-RFA-') || title.toLowerCase().includes('rfa');
const systemPrompt = `You are an expert Document Controller for a construction project (LCBP3) in Thailand.
The documents are primarily in THAI and ENGLISH.
Your task is to classify documents and extract metadata from OCR text.
Respond ONLY with valid JSON.`;
// Use pdfItem for the OCR extracted data, NOT the metaItem
const pdfText = String(pdfItem.json.data || '').substring(0, 3500).replace(/[^a-zA-Z0-9ก-๙\s\.\/\-:\[\]\(\)]/g, ' ');
const userPrompt = `Analyze this document:
[EXCEL METADATA]
Document Number: ${docNum || 'Not provided'}
Title: ${title || 'Not provided'}
Issued Date: ${issuedDate || 'Not provided'}
Received Date: ${receivedDate || 'Not provided'}
[DATABASE REFERENCES]
Disciplines: ${JSON.stringify(dbDisciplines)}
Tags: ${JSON.stringify(dbTags)}
[OCR TEXT EXTRACTION]
${pdfText}
Rules:
1. Category: Must be one of ${JSON.stringify(systemCategories)}. If Document Number contains "-RFA-", category MUST be "RFA".
2. Respond with EXACTLY 8 fields in JSON format:
- "discipline_id": Find 'id' from Disciplines array analyzing text to match 'th' or 'en'. If no match, use ID=64 (from contract LCBP3-C2).
- "subject": Document subject. If OCR is close to EXCEL METADATA Title, use EXCEL METADATA.
- "issued_date": Verify from OCR text if it matches ${issuedDate}, format YYYY-MM-DD.
- "received_date": Verify from OCR text. If empty, default to issued_date.
- "status": Extract status (e.g., For Information, Approve, Reject, Resubmit). This will be exported as "remark".
- "summary": 4-5 lines of Thai summary from OCR. This will be exported as "body".
- "tags": REQUIRED. Identify 2-5 main topics/themes from the document (from Title, subject matter, and OCR text). For each topic, return an object with:
* "tag_name": short topic name in Thai (2-5 words), e.g. "คอนกรีตผสม", "ทดสอบวัสดุ"
* "description": one sentence in Thai describing this topic (use key point details). e.g. "การทดสอบค่า slump ของคอนกรีตผสมที่หน้างาน"
Return as: [{"tag_name": "...", "description": "..."}, ...]
- "key_points": Array of 3-5 string key points extracted from the document (in Thai).
3. IMPORTANT: You MUST REPLACE the 'null' values in the template below with the actual Integer IDs or text you found. DO NOT reply with literal 'null' if you found a match!
Respond ONLY with this EXACT JSON structure:
{
"discipline_id": 64,
"subject": "${title}",
"issued_date": "${issuedDate}",
"received_date": "${receivedDate || issuedDate}",
"status": null,
"summary": "สรุปเนื้อหา 4-5 บรรทัด...",
"tags": [{"tag_name": "ชื่อหัวข้อ", "description": "คำอธิบาย key point ของหัวข้อนี้"}],
"key_points": ["จุดสำคัญที่ 1", "จุดสำคัญที่ 2", "จุดสำคัญที่ 3"],
"category": "${isRFA ? 'RFA' : 'Correspondence'}",
"confidence": 0.95
}`;
return {
json: {
...item.json,
active_model: model,
is_fallback: isFallback,
system_categories: systemCategories,
pre_mapped: {
project_id: projectId,
sender_id: senderId,
receiver_id: receiverId,
correspondence_type_id: corrTypeId
},
_debug_mapping: {
excel_project_code: projectCode,
excel_sender: senderCode,
excel_receiver: receiverCode,
excel_corr_type: corrType,
matched_project: dbProjects.find(p => p.code === projectCode || p.name === projectCode) || null,
first_org_sample: dbOrgs[0] || null
},
ollama_payload: {
model: model,
prompt: `${systemPrompt}\n\n${userPrompt}`,
stream: false,
format: 'json',
options: {
temperature: 0.1,
num_ctx: 8192
}
}
}
};
});
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
Organizations: [
{
id: 1,
organization_name: 'การท่าเรือแห่งประเทศไทย',
organization_code: 'กทท.'
},
{
id: 10,
organization_name: 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3',
organization_code: 'สคฉ.3'
},
{
id: 11,
organization_name: 'ตรวจรับพัสดุ ที่ปรึกษาควบคุมงาน',
organization_code: 'สคฉ.3-01'
},
{
id: 12,
organization_name: 'ตรวจรับพัสดุ งานทางทะเล',
organization_code: 'สคฉ.3-02'
},
{
id: 13,
organization_name: 'ตรวจรับพัสดุ อาคารและระบบสาธารณูปโภค',
organization_code: 'สคฉ.3-03'
}
]
Projects: [
{
id: 1,
project_code: 'LCBP3',
project_name: 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)'
},
{
id: 2,
project_code: 'LCBP3-C1',
project_name: 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1) งานก่อสร้างงานทางทะเล'
},
{
id: 3,
project_code: 'LCBP3-C2',
project_name: 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และระบบสาธารณูปโภค'
},
{
id: 4,
project_code: 'LCBP3-C3',
project_name: 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 3) งานก่อสร้าง'
},
{
id: 5,
project_code: 'LCBP3-C4',
project_name: 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง'
}
]
Correspondence Types: [
{ id: 1, type_code: 'RFA', type_name: 'Request for Approval' },
{ id: 2, type_code: 'RFI', type_name: 'Request for Information' },
{ id: 3, type_code: 'TRANSMITTAL', type_name: 'Transmittal' },
{ id: 4, type_code: 'EMAIL', type_name: 'Email' },
{ id: 5, type_code: 'INSTRUCTION', type_name: 'Instruction' }
]
+9
View File
@@ -0,0 +1,9 @@
SELECT 'projects' as type, id, project_code as text1, project_name as text2 FROM projects
UNION ALL
SELECT 'disciplines' as type, id, code_name_th as text1, code_name_en as text2 FROM disciplines
UNION ALL
SELECT 'organizations' as type, id, organization_name as text1, organization_code as text2 FROM organizations
UNION ALL
SELECT 'tags' as type, id, tag_name as text1, description as text2 FROM tags
UNION ALL
SELECT 'correspondence_types' as type, id, type_code as text1, type_name as text2 FROM correspondence_types
@@ -440,47 +440,22 @@ CREATE TABLE rfas (
-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ rfas (1:N) -- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ rfas (1:N)
CREATE TABLE rfa_revisions ( CREATE TABLE rfa_revisions (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', id INT PRIMARY KEY COMMENT 'ID (แชร์กับ correspondence_revisions)',
rfa_id INT NOT NULL COMMENT 'Master ID ของ RFA',
revision_number INT NOT NULL COMMENT 'หมายเลข Revision (0, 1, 2...)',
revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)',
is_current BOOLEAN DEFAULT FALSE COMMENT '(1 = Revision ปัจจุบัน)',
-- ข้อมูลเฉพาะของ RFA Revision ที่ซับซ้อน
rfa_status_code_id INT NOT NULL COMMENT 'สถานะ RFA', rfa_status_code_id INT NOT NULL COMMENT 'สถานะ RFA',
rfa_approve_code_id INT COMMENT 'ผลการอนุมัติ', rfa_approve_code_id INT COMMENT 'ผลการอนุมัติ',
subject VARCHAR(500) NOT NULL COMMENT 'หัวข้อเรื่อง',
description TEXT COMMENT 'คำอธิบายการแก้ไขใน Revision นี้',
body TEXT NULL COMMENT 'เนื้อความ (ถ้ามี)',
remarks TEXT COMMENT 'หมายเหตุ',
document_date DATE COMMENT 'วันที่ในเอกสาร',
issued_date DATE COMMENT 'วันที่ส่งขออนุมัติ',
received_date DATETIME COMMENT 'วันที่ลงรับเอกสาร',
due_date DATETIME COMMENT 'วันที่ครบกำหนด',
approved_date DATE COMMENT 'วันที่อนุมัติ', approved_date DATE COMMENT 'วันที่อนุมัติ',
-- Standard Meta Columns
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้างเอกสาร',
created_by INT COMMENT 'ผู้สร้าง',
updated_by INT COMMENT 'ผู้แก้ไขล่าสุด',
-- ส่วนของ JSON และ Schema Version
details JSON NULL COMMENT 'RFA Specific Details', details JSON NULL COMMENT 'RFA Specific Details',
schema_version INT DEFAULT 1 COMMENT 'Version ของ JSON Schema', schema_version INT DEFAULT 1 COMMENT 'Version ของ JSON Schema',
-- Generated Virtual Columns (ดึงค่าจาก JSON โดยอัตโนมัติ)
v_ref_drawing_count INT GENERATED ALWAYS AS ( v_ref_drawing_count INT GENERATED ALWAYS AS (
JSON_UNQUOTE( JSON_UNQUOTE(
JSON_EXTRACT(details, '$.drawingCount') JSON_EXTRACT(details, '$.drawingCount')
) )
) VIRTUAL, ) VIRTUAL,
FOREIGN KEY (rfa_id) REFERENCES rfas (id) ON DELETE CASCADE, FOREIGN KEY (id) REFERENCES correspondence_revisions (id) ON DELETE CASCADE,
FOREIGN KEY (rfa_status_code_id) REFERENCES rfa_status_codes (id), FOREIGN KEY (rfa_status_code_id) REFERENCES rfa_status_codes (id),
FOREIGN KEY (rfa_approve_code_id) REFERENCES rfa_approve_codes (id) ON DELETE FOREIGN KEY (rfa_approve_code_id) REFERENCES rfa_approve_codes (id) ON DELETE
SET NULL, SET NULL
FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางขยายของ correspondence_revisions สำหรับ RFA (1:1)';
SET NULL,
FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE
SET NULL,
UNIQUE KEY uq_rr_rev_number (rfa_id, revision_number),
UNIQUE KEY uq_rr_current (rfa_id, is_current)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ rfas (1 :N)';
-- ตารางเชื่อมระหว่าง rfa_revisions (ที่เป็นประเภท DWG) กับ shop_drawing_revisions (M:N) -- ตารางเชื่อมระหว่าง rfa_revisions (ที่เป็นประเภท DWG) กับ shop_drawing_revisions (M:N)
CREATE TABLE rfa_items ( CREATE TABLE rfa_items (
@@ -3,6 +3,7 @@
-- รัน: หลังจาก 02-schema-tables.sql เสร็จ -- รัน: หลังจาก 02-schema-tables.sql เสร็จ
-- ========================================================== -- ==========================================================
SET NAMES utf8mb4; SET NAMES utf8mb4;
SET time_zone = '+07:00'; SET time_zone = '+07:00';
-- ============================================================ -- ============================================================
@@ -176,12 +177,12 @@ SELECT r.id AS rfa_id,
c.originator_id, c.originator_id,
org.organization_name AS originator_name, org.organization_name AS originator_name,
rr.id AS revision_id, rr.id AS revision_id,
rr.revision_number, cr.revision_number,
rr.revision_label, cr.revision_label,
rr.subject, cr.subject,
rr.document_date, cr.document_date,
rr.issued_date, cr.issued_date,
rr.received_date, cr.received_date,
rr.approved_date, rr.approved_date,
rr.rfa_status_code_id, rr.rfa_status_code_id,
rsc.status_code AS rfa_status_code, rsc.status_code AS rfa_status_code,
@@ -189,21 +190,22 @@ SELECT r.id AS rfa_id,
rr.rfa_approve_code_id, rr.rfa_approve_code_id,
rac.approve_code AS rfa_approve_code, rac.approve_code AS rfa_approve_code,
rac.approve_name AS rfa_approve_name, rac.approve_name AS rfa_approve_name,
rr.created_by, cr.created_by,
u.username AS created_by_username, u.username AS created_by_username,
rr.created_at AS revision_created_at cr.created_at AS revision_created_at
FROM rfas r FROM rfas r
INNER JOIN rfa_types rt ON r.rfa_type_id = rt.id INNER JOIN rfa_types rt ON r.rfa_type_id = rt.id
INNER JOIN rfa_revisions rr ON r.id = rr.rfa_id -- RFA uses shared primary key with correspondences (1:1) INNER JOIN correspondences c ON r.id = c.id
INNER JOIN correspondences c ON r.id = c.id -- [FIX 1] เพิ่มการ Join ตาราง disciplines INNER JOIN correspondence_revisions cr ON c.id = cr.correspondence_id
INNER JOIN rfa_revisions rr ON cr.id = rr.id -- RFA uses shared primary key with correspondence_revisions (1:1)
-- [FIX 1] เพิ่มการ Join ตาราง disciplines
LEFT JOIN disciplines d ON c.discipline_id = d.id LEFT JOIN disciplines d ON c.discipline_id = d.id
INNER JOIN projects p ON c.project_id = p.id INNER JOIN projects p ON c.project_id = p.id
INNER JOIN organizations org ON c.originator_id = org.id INNER JOIN organizations org ON c.originator_id = org.id
INNER JOIN rfa_status_codes rsc ON rr.rfa_status_code_id = rsc.id INNER JOIN rfa_status_codes rsc ON rr.rfa_status_code_id = rsc.id
LEFT JOIN rfa_approve_codes rac ON rr.rfa_approve_code_id = rac.id LEFT JOIN rfa_approve_codes rac ON rr.rfa_approve_code_id = rac.id
LEFT JOIN users u ON rr.created_by = u.user_id LEFT JOIN users u ON cr.created_by = u.user_id
WHERE rr.is_current = TRUE WHERE cr.is_current = TRUE
AND r.deleted_at IS NULL
AND c.deleted_at IS NULL; AND c.deleted_at IS NULL;
-- View แสดงความสัมพันธ์ทั้งหมดระหว่าง Contract, Project, และ Organization -- View แสดงความสัมพันธ์ทั้งหมดระหว่าง Contract, Project, และ Organization
@@ -249,7 +251,7 @@ SELECT -- 1. Workflow Instance Info
ELSE 'N/A' ELSE 'N/A'
END AS document_number, END AS document_number,
CASE CASE
WHEN wi.entity_type = 'rfa_revision' THEN rfa_rev.subject WHEN wi.entity_type = 'rfa_revision' THEN rfa_corr_rev.subject
WHEN wi.entity_type = 'circulation' THEN circ.circulation_subject WHEN wi.entity_type = 'circulation' THEN circ.circulation_subject
WHEN wi.entity_type = 'correspondence_revision' THEN corr_rev.subject WHEN wi.entity_type = 'correspondence_revision' THEN corr_rev.subject
ELSE 'Unknown Document' ELSE 'Unknown Document'
@@ -262,7 +264,8 @@ FROM workflow_instances wi
JOIN workflow_definitions wd ON wi.definition_id = wd.id -- 5. Joins for RFA (ซับซ้อนหน่อยเพราะ RFA ผูกกับ Correspondence อีกที) JOIN workflow_definitions wd ON wi.definition_id = wd.id -- 5. Joins for RFA (ซับซ้อนหน่อยเพราะ RFA ผูกกับ Correspondence อีกที)
LEFT JOIN rfa_revisions rfa_rev ON wi.entity_type = 'rfa_revision' LEFT JOIN rfa_revisions rfa_rev ON wi.entity_type = 'rfa_revision'
AND wi.entity_id = CAST(rfa_rev.id AS CHAR) AND wi.entity_id = CAST(rfa_rev.id AS CHAR)
LEFT JOIN correspondences rfa_corr ON rfa_rev.id = rfa_corr.id -- 6. Joins for Circulation LEFT JOIN correspondence_revisions rfa_corr_rev ON rfa_rev.id = rfa_corr_rev.id
LEFT JOIN correspondences rfa_corr ON rfa_corr_rev.correspondence_id = rfa_corr.id -- 6. Joins for Circulation
LEFT JOIN circulations circ ON wi.entity_type = 'circulation' LEFT JOIN circulations circ ON wi.entity_type = 'circulation'
AND wi.entity_id = CAST(circ.id AS CHAR) -- 7. Joins for Correspondence AND wi.entity_id = CAST(circ.id AS CHAR) -- 7. Joins for Correspondence
LEFT JOIN correspondence_revisions corr_rev ON wi.entity_type = 'correspondence_revision' LEFT JOIN correspondence_revisions corr_rev ON wi.entity_type = 'correspondence_revision'
@@ -497,9 +500,7 @@ CREATE INDEX idx_corr_revisions_current_status ON correspondence_revisions (is_c
CREATE INDEX idx_corr_revisions_correspondence_current ON correspondence_revisions (correspondence_id, is_current); CREATE INDEX idx_corr_revisions_correspondence_current ON correspondence_revisions (correspondence_id, is_current);
-- Indexes for v_current_rfas performance -- Indexes for v_current_rfas performance
CREATE INDEX idx_rfa_revisions_current_status ON rfa_revisions (is_current, rfa_status_code_id); CREATE INDEX idx_rfa_revisions_status ON rfa_revisions (rfa_status_code_id);
CREATE INDEX idx_rfa_revisions_rfa_current ON rfa_revisions (rfa_id, is_current);
-- Indexes for document statistics performance -- Indexes for document statistics performance
CREATE INDEX idx_correspondences_project_type ON correspondences (project_id, correspondence_type_id); CREATE INDEX idx_correspondences_project_type ON correspondences (project_id, correspondence_type_id);
@@ -513,4 +514,3 @@ CREATE INDEX IDX_AUDIT_STATUS ON document_number_audit (STATUS);
CREATE INDEX IDX_AUDIT_OPERATION ON document_number_audit (operation); CREATE INDEX IDX_AUDIT_OPERATION ON document_number_audit (operation);
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;
@@ -1924,56 +1924,6 @@ VALUES (
NULL NULL
); );
INSERT INTO `rfas` (
`id`,
`rfa_type_id`,
`created_at`,
`created_by`,
`deleted_at`
)
VALUES (2, 68, '2025-12-06 05:40:02', 1, NULL);
INSERT INTO `rfa_revisions` (
`id`,
`rfa_id`,
`revision_number`,
`revision_label`,
`is_current`,
`rfa_status_code_id`,
`rfa_approve_code_id`,
`subject`,
`document_date`,
`issued_date`,
`received_date`,
`approved_date`,
`description`,
`details`,
`schema_version`,
`created_at`,
`created_by`,
`updated_by`
)
VALUES (
1,
2,
1,
'A',
0,
2,
NULL,
'ขออนุมัติผลการทดสอบเสาเข็มแบบ Dynamic Load Test สำหรับงานเสาเข็มตอกของอาคารสถานีไฟฟ้าย่อย 22 kV. No. 6',
'2025-12-03',
'2025-12-04',
'2025-12-04 12:40:19',
NULL,
NULL,
NULL,
1,
'2025-12-06 05:41:25',
NULL,
NULL
);
-- ========================================================== -- ==========================================================
-- 20. Workflow Definitions (Unified Workflow Engine) -- 20. Workflow Definitions (Unified Workflow Engine)
-- ========================================================== -- ==========================================================
File diff suppressed because one or more lines are too long
+16
View File
@@ -0,0 +1,16 @@
{
"name": "03-data-and-storage",
"version": "1.0.0",
"description": "",
"main": ".mountcheck.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"mysql2": "^3.19.1"
}
}
+28
View File
@@ -0,0 +1,28 @@
const mysql = require('mysql2/promise');
async function test() {
const connection = await mysql.createConnection({
host: '192.168.10.8',
port: 3306,
user: 'migration_bot',
password: 'Center2025',
database: 'lcbp3'
});
try {
const [orgs] = await connection.execute('SELECT id, organization_name, organization_code FROM organizations');
console.log('Organizations:', orgs.slice(0, 5));
const [projects] = await connection.execute('SELECT id, project_code, project_name FROM projects');
console.log('Projects:', projects.slice(0, 5));
const [corrTypes] = await connection.execute('SELECT id, type_code, type_name FROM correspondence_types');
console.log('Correspondence Types:', corrTypes.slice(0, 5));
} catch (err) {
console.error(err);
} finally {
await connection.end();
}
}
test();
File diff suppressed because it is too large Load Diff