251129:1700 update to 1.4.5

This commit is contained in:
admin
2025-11-29 16:50:34 +07:00
parent f7a43600a3
commit a78c9941be
55 changed files with 14641 additions and 2090 deletions

View File

@@ -1,42 +1,41 @@
// File: src/modules/rfa/rfa.service.ts
import {
Injectable,
NotFoundException,
InternalServerErrorException,
Logger,
BadRequestException,
ForbiddenException,
Injectable,
InternalServerErrorException,
Logger,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In } from 'typeorm';
import { DataSource, In, Repository } from 'typeorm';
// Entities
import { Rfa } from './entities/rfa.entity.js';
import { RfaRevision } from './entities/rfa-revision.entity.js';
import { RfaItem } from './entities/rfa-item.entity.js';
import { RfaType } from './entities/rfa-type.entity.js';
import { RfaStatusCode } from './entities/rfa-status-code.entity.js';
import { RfaApproveCode } from './entities/rfa-approve-code.entity.js';
import { Correspondence } from '../correspondence/entities/correspondence.entity.js';
import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity.js';
import { RoutingTemplate } from '../correspondence/entities/routing-template.entity.js';
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity.js';
import { User } from '../user/entities/user.entity.js';
import { CorrespondenceRouting } from '../correspondence/entities/correspondence-routing.entity';
import { Correspondence } from '../correspondence/entities/correspondence.entity';
import { RoutingTemplate } from '../correspondence/entities/routing-template.entity';
import { ShopDrawingRevision } from '../drawing/entities/shop-drawing-revision.entity';
import { User } from '../user/entities/user.entity';
import { RfaApproveCode } from './entities/rfa-approve-code.entity';
import { RfaItem } from './entities/rfa-item.entity';
import { RfaRevision } from './entities/rfa-revision.entity';
import { RfaStatusCode } from './entities/rfa-status-code.entity';
import { RfaType } from './entities/rfa-type.entity';
import { Rfa } from './entities/rfa.entity';
// DTOs
import { CreateRfaDto } from './dto/create-rfa.dto.js';
import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto.js';
import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto';
import { CreateRfaDto } from './dto/create-rfa.dto';
// Interfaces & Enums
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface.js';
// Services
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
import { UserService } from '../user/user.service.js';
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service.js';
import { NotificationService } from '../notification/notification.service.js';
import { SearchService } from '../search/search.service.js';
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
import { NotificationService } from '../notification/notification.service';
import { SearchService } from '../search/search.service';
import { UserService } from '../user/user.service';
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
@Injectable()
export class RfaService {
@@ -87,6 +86,7 @@ export class RfaService {
);
}
// Determine User Organization
let userOrgId = user.primaryOrganizationId;
if (!userOrgId) {
const fullUser = await this.userService.findOne(user.user_id);
@@ -101,14 +101,14 @@ export class RfaService {
await queryRunner.startTransaction();
try {
const orgCode = 'ORG'; // TODO: Fetch real ORG Code
const orgCode = 'ORG'; // TODO: Fetch real ORG Code from Org Service if needed
// [FIXED] เรียกใช้แบบ Object Context พร้อม disciplineId
// [UPDATED] Generate Document Number with Discipline
const docNumber = await this.numberingService.generateNextNumber({
projectId: createDto.projectId,
originatorId: userOrgId,
typeId: createDto.rfaTypeId, // RFA Type ใช้เป็น ID ในการนับเลข
disciplineId: createDto.disciplineId, // สำคัญมากสำหรับ RFA (Req 6B)
typeId: createDto.rfaTypeId,
disciplineId: createDto.disciplineId ?? 0, // ✅ ส่ง disciplineId ไปด้วย (0 ถ้าไม่มี)
year: new Date().getFullYear(),
customTokens: {
TYPE_CODE: rfaType.typeCode,
@@ -116,24 +116,31 @@ export class RfaService {
},
});
// 1. Create Correspondence Record
const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber,
correspondenceTypeId: createDto.rfaTypeId, // Map RFA Type to Corr Type ID
disciplineId: createDto.disciplineId, // บันทึก Discipline
correspondenceTypeId: createDto.rfaTypeId, // Assuming RFA Type maps directly or via logic
// Note: ถ้า CorrespondenceType แยก ID กับ RFA Type ต้อง Map ให้ถูก
// ในที่นี้สมมติว่าใช้ ID เดียวกัน หรือ RFA Type เป็น SubType ของ Correspondence
projectId: createDto.projectId,
originatorId: userOrgId,
isInternal: false,
isInternalCommunication: false,
createdBy: user.user_id,
// ✅ Add disciplineId column if correspondence table supports it (as per Data Dictionary Update)
// disciplineId: createDto.disciplineId
});
const savedCorr = await queryRunner.manager.save(correspondence);
// 2. Create RFA Master Record
const rfa = queryRunner.manager.create(Rfa, {
rfaTypeId: createDto.rfaTypeId,
disciplineId: createDto.disciplineId, // บันทึก Discipline
createdBy: user.user_id,
// ✅ ถ้า Entity Rfa มี disciplineId ให้ใส่ตรงนี้ด้วย
// disciplineId: createDto.disciplineId
});
const savedRfa = await queryRunner.manager.save(rfa);
// 3. Create First Revision (Draft)
const rfaRevision = queryRunner.manager.create(RfaRevision, {
correspondenceId: savedCorr.id,
rfaId: savedRfa.id,
@@ -147,9 +154,12 @@ export class RfaService {
? new Date(createDto.documentDate)
: new Date(),
createdBy: user.user_id,
details: createDto.details, // ✅ Save JSON Details
schemaVersion: 1, // ✅ Default Schema Version
});
const savedRevision = await queryRunner.manager.save(rfaRevision);
// 4. Link Shop Drawings
if (
createDto.shopDrawingRevisionIds &&
createDto.shopDrawingRevisionIds.length > 0
@@ -164,7 +174,8 @@ export class RfaService {
const rfaItems = shopDrawings.map((sd) =>
queryRunner.manager.create(RfaItem, {
rfaRevisionId: savedCorr.id,
rfaRevisionId: savedCorr.id, // ใช้ ID ของ Correspondence (ตาม Schema ที่ออกแบบไว้) หรือ RFA Revision ID แล้วแต่การ Map Entity
// ตาม Entity RfaItem ที่ให้มา: rfaRevisionId map ไปที่ correspondence_id
shopDrawingRevisionId: sd.id,
}),
);
@@ -173,9 +184,10 @@ export class RfaService {
await queryRunner.commitTransaction();
// Indexing for Search
this.searchService.indexDocument({
id: savedCorr.id,
type: 'correspondence',
type: 'rfa',
docNumber: docNumber,
title: createDto.title,
description: createDto.description,
@@ -186,10 +198,8 @@ export class RfaService {
return {
...savedRfa,
currentRevision: {
...savedRevision,
correspondenceNumber: docNumber,
},
correspondenceNumber: docNumber,
currentRevision: savedRevision,
};
} catch (err) {
await queryRunner.rollbackTransaction();
@@ -200,7 +210,8 @@ export class RfaService {
}
}
// ... (method อื่นๆ findOne, submit, processAction คงเดิม)
// ... (ส่วน findOne, submit, processAction คงเดิมจากไฟล์ที่แนบมา แค่ปรับปรุงเล็กน้อยตาม Context) ...
async findOne(id: number) {
const rfa = await this.rfaRepo.findOne({
where: { id },
@@ -230,14 +241,8 @@ export class RfaService {
const rfa = await this.findOne(rfaId);
const currentRevision = rfa.revisions.find((r) => r.isCurrent);
if (!currentRevision) {
if (!currentRevision)
throw new NotFoundException('Current revision not found');
}
if (!currentRevision.correspondence) {
throw new InternalServerErrorException('Correspondence relation missing');
}
if (currentRevision.statusCode.statusCode !== 'DFT') {
throw new BadRequestException('Only DRAFT documents can be submitted');
}
@@ -255,21 +260,21 @@ export class RfaService {
const statusForApprove = await this.rfaStatusRepo.findOne({
where: { statusCode: 'FAP' },
});
if (!statusForApprove) {
if (!statusForApprove)
throw new InternalServerErrorException('Status FAP not found');
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Update Revision Status
currentRevision.rfaStatusCodeId = statusForApprove.id;
currentRevision.issuedDate = new Date();
await queryRunner.manager.save(currentRevision);
// Create First Routing Step
const firstStep = template.steps[0];
const routing = queryRunner.manager.create(CorrespondenceRouting, {
correspondenceId: currentRevision.correspondenceId,
templateId: template.id,
@@ -286,20 +291,18 @@ export class RfaService {
});
await queryRunner.manager.save(routing);
// Notify
const recipientUserId = await this.userService.findDocControlIdByOrg(
firstStep.toOrganizationId,
);
if (recipientUserId) {
const docNo = currentRevision.correspondence.correspondenceNumber;
await this.notificationService.send({
userId: recipientUserId,
title: `RFA Submitted: ${currentRevision.title}`,
message: `มีเอกสาร RFA ใหม่รอการตรวจสอบจากคุณ (เลขที่: ${docNo})`,
message: `RFA ${currentRevision.correspondence.correspondenceNumber} submitted for approval.`,
type: 'SYSTEM',
entityType: 'rfa',
entityId: rfa.id,
link: `/rfas/${rfa.id}`,
});
}
@@ -307,7 +310,6 @@ export class RfaService {
return { message: 'RFA Submitted successfully', routing };
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(`Failed to submit RFA: ${(err as Error).message}`);
throw err;
} finally {
await queryRunner.release();
@@ -315,11 +317,13 @@ 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) {
if (!currentRevision)
throw new NotFoundException('Current revision not found');
}
const currentRouting = await this.routingRepo.findOne({
where: {
@@ -330,10 +334,8 @@ export class RfaService {
relations: ['toOrganization'],
});
if (!currentRouting) {
if (!currentRouting)
throw new BadRequestException('No active workflow step found');
}
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
throw new ForbiddenException(
'You are not authorized to process this step',
@@ -345,10 +347,10 @@ export class RfaService {
relations: ['steps'],
});
if (!template || !template.steps) {
throw new InternalServerErrorException('Template or steps not found');
}
if (!template || !template.steps)
throw new InternalServerErrorException('Template not found');
// Call Engine to calculate next step
const result = this.workflowEngine.processAction(
currentRouting.sequence,
template.steps.length,
@@ -361,19 +363,19 @@ export class RfaService {
await queryRunner.startTransaction();
try {
currentRouting.status =
dto.action === WorkflowAction.REJECT ? 'REJECTED' : 'ACTIONED';
// Update current routing
currentRouting.status = dto.action === 'REJECT' ? 'REJECTED' : 'ACTIONED';
currentRouting.processedByUserId = user.user_id;
currentRouting.processedAt = new Date();
currentRouting.comments = dto.comments;
await queryRunner.manager.save(currentRouting);
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
const nextStepConfig = template.steps.find(
// Create next routing if available
if (result.nextStepSequence && dto.action !== 'REJECT') {
const nextStep = template.steps.find(
(s) => s.sequence === result.nextStepSequence,
);
if (nextStepConfig) {
if (nextStep) {
const nextRouting = queryRunner.manager.create(
CorrespondenceRouting,
{
@@ -381,52 +383,43 @@ export class RfaService {
templateId: template.id,
sequence: result.nextStepSequence,
fromOrganizationId: user.primaryOrganizationId,
toOrganizationId: nextStepConfig.toOrganizationId,
stepPurpose: nextStepConfig.stepPurpose,
toOrganizationId: nextStep.toOrganizationId,
stepPurpose: nextStep.stepPurpose,
status: 'SENT',
dueDate: new Date(
Date.now() +
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000,
Date.now() + (nextStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
),
},
);
await queryRunner.manager.save(nextRouting);
}
} else if (
result.nextStepSequence === null &&
dto.action !== WorkflowAction.REJECT
) {
const approveCodeStr =
dto.action === WorkflowAction.APPROVE ? '1A' : '4X';
const approveCode = await this.rfaApproveRepo.findOne({
where: { approveCode: approveCodeStr },
});
if (approveCode) {
currentRevision.rfaApproveCodeId = approveCode.id;
currentRevision.approvedDate = new Date();
}
await queryRunner.manager.save(currentRevision);
} else if (dto.action === WorkflowAction.REJECT) {
const rejectCode = await this.rfaApproveRepo.findOne({
where: { approveCode: '4X' },
});
if (rejectCode) {
currentRevision.rfaApproveCodeId = rejectCode.id;
} else if (result.nextStepSequence === null) {
// Workflow Ended (Completed or Rejected)
// Update RFA Status (Approved/Rejected Code)
if (dto.action !== 'REJECT') {
const approveCode = await this.rfaApproveRepo.findOne({
where: { approveCode: dto.action === 'APPROVE' ? '1A' : '4X' },
}); // Logic Map Code อย่างง่าย
if (approveCode) {
currentRevision.rfaApproveCodeId = approveCode.id;
currentRevision.approvedDate = new Date();
}
} else {
const rejectCode = await this.rfaApproveRepo.findOne({
where: { approveCode: '4X' },
});
if (rejectCode) currentRevision.rfaApproveCodeId = rejectCode.id;
}
await queryRunner.manager.save(currentRevision);
}
await queryRunner.commitTransaction();
return { message: 'Action processed successfully', result };
return { message: 'Action processed', result };
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Failed to process RFA action: ${(err as Error).message}`,
);
throw err;
} finally {
await queryRunner.release();
}
}
}
}