251204:1700 Prepare to version 1.5.1
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class V1_5_1_Schema_Update1701676800000 implements MigrationInterface {
|
||||
name = 'V1_5_1_Schema_Update1701676800000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 1. Create Disciplines Table
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`disciplines\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||
\`contract_id\` INT NOT NULL COMMENT 'ผูกกับสัญญา',
|
||||
\`discipline_code\` VARCHAR(10) NOT NULL COMMENT 'รหัสสาขา (เช่น GEN, STR)',
|
||||
\`code_name_th\` VARCHAR(255) COMMENT 'ชื่อไทย',
|
||||
\`code_name_en\` VARCHAR(255) COMMENT 'ชื่ออังกฤษ',
|
||||
\`is_active\` TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน',
|
||||
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (\`contract_id\`) REFERENCES \`contracts\` (\`id\`) ON DELETE CASCADE,
|
||||
UNIQUE KEY \`uk_discipline_contract\` (\`contract_id\`, \`discipline_code\`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บข้อมูลสาขางาน (Disciplines) ตาม Req 6B';
|
||||
`);
|
||||
|
||||
// 2. Create Correspondence Sub Types Table
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`correspondence_sub_types\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||
\`contract_id\` INT NOT NULL COMMENT 'ผูกกับสัญญา',
|
||||
\`correspondence_type_id\` INT NOT NULL COMMENT 'ผูกกับประเภทเอกสารหลัก (เช่น RFA)',
|
||||
\`sub_type_code\` VARCHAR(20) NOT NULL COMMENT 'รหัสย่อย (เช่น MAT, SHP)',
|
||||
\`sub_type_name\` VARCHAR(255) COMMENT 'ชื่อประเภทหนังสือย่อย',
|
||||
\`sub_type_number\` VARCHAR(10) COMMENT 'เลขรหัสสำหรับ Running Number (เช่น 11, 22)',
|
||||
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (\`contract_id\`) REFERENCES \`contracts\` (\`id\`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (\`correspondence_type_id\`) REFERENCES \`correspondence_types\` (\`id\`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บประเภทหนังสือย่อย (Sub Types) ตาม Req 6B';
|
||||
`);
|
||||
|
||||
// 3. Add discipline_id to correspondences
|
||||
const hasDisciplineCol = await queryRunner.hasColumn(
|
||||
'correspondences',
|
||||
'discipline_id'
|
||||
);
|
||||
if (!hasDisciplineCol) {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`correspondences\`
|
||||
ADD COLUMN \`discipline_id\` INT NULL COMMENT 'สาขางาน (ถ้ามี)' AFTER \`correspondence_type_id\`,
|
||||
ADD CONSTRAINT \`fk_corr_discipline\` FOREIGN KEY (\`discipline_id\`) REFERENCES \`disciplines\` (\`id\`) ON DELETE SET NULL;
|
||||
`);
|
||||
}
|
||||
|
||||
// 4. Add discipline_id to rfas
|
||||
const hasRfaDisciplineCol = await queryRunner.hasColumn(
|
||||
'rfas',
|
||||
'discipline_id'
|
||||
);
|
||||
if (!hasRfaDisciplineCol) {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`rfas\`
|
||||
ADD COLUMN \`discipline_id\` INT NULL COMMENT 'สาขางาน (ถ้ามี)' AFTER \`rfa_type_id\`,
|
||||
ADD CONSTRAINT \`fk_rfa_discipline\` FOREIGN KEY (\`discipline_id\`) REFERENCES \`disciplines\` (\`id\`) ON DELETE SET NULL;
|
||||
`);
|
||||
}
|
||||
|
||||
// 5. Create Document Numbering Audit & Errors
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`document_number_audit\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
\`project_id\` INT NOT NULL,
|
||||
\`correspondence_type_id\` INT NOT NULL,
|
||||
\`running_number\` INT NOT NULL,
|
||||
\`full_document_number\` VARCHAR(100) NOT NULL,
|
||||
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
\`created_by\` INT NULL
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`document_number_errors\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
\`error_code\` VARCHAR(50),
|
||||
\`error_message\` TEXT,
|
||||
\`context_data\` JSON,
|
||||
\`occurred_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
// 6. Create RFA Items (Linking RFA to Shop Drawings)
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`rfa_items\` (
|
||||
\`rfarev_correspondence_id\` INT COMMENT 'ID ของ RFA Revision',
|
||||
\`shop_drawing_revision_id\` INT COMMENT 'ID ของ Shop Drawing Revision',
|
||||
PRIMARY KEY (\`rfarev_correspondence_id\`, \`shop_drawing_revision_id\`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
// 7. Create Transmittal Tables
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`transmittals\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||
\`correspondence_id\` INT UNIQUE COMMENT 'ID ของเอกสาร',
|
||||
\`transmittal_no\` VARCHAR(100) NOT NULL COMMENT 'เลขที่ใบนำส่ง',
|
||||
\`subject\` VARCHAR(500) NOT NULL COMMENT 'เรื่อง',
|
||||
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (\`correspondence_id\`) REFERENCES \`correspondences\` (\`id\`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`transmittal_items\` (
|
||||
\`transmittal_id\` INT NOT NULL,
|
||||
\`item_type\` VARCHAR(50) NOT NULL COMMENT 'ประเภทรายการ (DRAWING, RFA, etc.)',
|
||||
\`item_id\` INT NOT NULL COMMENT 'ID ของรายการ',
|
||||
\`description\` TEXT,
|
||||
PRIMARY KEY (\`transmittal_id\`, \`item_type\`, \`item_id\`),
|
||||
FOREIGN KEY (\`transmittal_id\`) REFERENCES \`transmittals\` (\`id\`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
// 8. Create Circulation Tables
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`circulation_status_codes\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
\`code\` VARCHAR(20) NOT NULL UNIQUE,
|
||||
\`description\` VARCHAR(50) NOT NULL,
|
||||
\`sort_order\` INT DEFAULT 0,
|
||||
\`is_active\` TINYINT(1) DEFAULT 1
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`circulations\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
\`correspondence_id\` INT UNIQUE,
|
||||
\`organization_id\` INT NOT NULL,
|
||||
\`circulation_no\` VARCHAR(100) NOT NULL,
|
||||
\`circulation_subject\` VARCHAR(500) NOT NULL,
|
||||
\`circulation_status_code\` VARCHAR(20) NOT NULL,
|
||||
\`created_by_user_id\` INT NOT NULL,
|
||||
\`submitted_at\` TIMESTAMP NULL,
|
||||
\`closed_at\` TIMESTAMP NULL,
|
||||
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (\`correspondence_id\`) REFERENCES \`correspondences\` (\`id\`),
|
||||
FOREIGN KEY (\`organization_id\`) REFERENCES \`organizations\` (\`id\`),
|
||||
FOREIGN KEY (\`circulation_status_code\`) REFERENCES \`circulation_status_codes\` (\`code\`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`circulation_attachments\` (
|
||||
\`circulation_id\` INT NOT NULL,
|
||||
\`attachment_id\` INT NOT NULL,
|
||||
PRIMARY KEY (\`circulation_id\`, \`attachment_id\`),
|
||||
FOREIGN KEY (\`circulation_id\`) REFERENCES \`circulations\` (\`id\`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Drop tables in reverse order
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS circulation_attachments`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS circulations`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS circulation_status_codes`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS transmittal_items`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS transmittals`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS rfa_items`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS document_number_errors`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS document_number_audit`);
|
||||
|
||||
// Remove columns
|
||||
const hasRfaDisciplineCol = await queryRunner.hasColumn(
|
||||
'rfas',
|
||||
'discipline_id'
|
||||
);
|
||||
if (hasRfaDisciplineCol) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE rfas DROP FOREIGN KEY fk_rfa_discipline`
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE rfas DROP COLUMN discipline_id`);
|
||||
}
|
||||
|
||||
const hasDisciplineCol = await queryRunner.hasColumn(
|
||||
'correspondences',
|
||||
'discipline_id'
|
||||
);
|
||||
if (hasDisciplineCol) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE correspondences DROP FOREIGN KEY fk_corr_discipline`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE correspondences DROP COLUMN discipline_id`
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS correspondence_sub_types`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS disciplines`);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Circulation } from './entities/circulation.entity';
|
||||
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module';
|
||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||
import { CirculationWorkflowService } from './circulation-workflow.service';
|
||||
import { CirculationController } from './circulation.controller';
|
||||
import { CirculationService } from './circulation.service';
|
||||
@@ -20,6 +21,7 @@ import { CirculationService } from './circulation.service';
|
||||
]),
|
||||
UserModule,
|
||||
WorkflowEngineModule,
|
||||
DocumentNumberingModule,
|
||||
],
|
||||
controllers: [CirculationController],
|
||||
providers: [CirculationService, CirculationWorkflowService],
|
||||
|
||||
@@ -5,14 +5,15 @@ import {
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, Not } from 'typeorm'; // เพิ่ม Not
|
||||
import { Repository, DataSource, Not } from 'typeorm';
|
||||
|
||||
import { Circulation } from './entities/circulation.entity';
|
||||
import { CirculationRouting } from './entities/circulation-routing.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { CreateCirculationDto } from './dto/create-circulation.dto';
|
||||
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto'; // Import ใหม่
|
||||
import { SearchCirculationDto } from './dto/search-circulation.dto'; // Import ใหม่
|
||||
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto';
|
||||
import { SearchCirculationDto } from './dto/search-circulation.dto';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
|
||||
@Injectable()
|
||||
export class CirculationService {
|
||||
@@ -21,7 +22,8 @@ export class CirculationService {
|
||||
private circulationRepo: Repository<Circulation>,
|
||||
@InjectRepository(CirculationRouting)
|
||||
private routingRepo: Repository<CirculationRouting>,
|
||||
private dataSource: DataSource,
|
||||
private numberingService: DocumentNumberingService,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateCirculationDto, user: User) {
|
||||
@@ -34,8 +36,17 @@ export class CirculationService {
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// Generate No. (Mock Logic) -> ควรใช้ NumberingService จริงในอนาคต
|
||||
const circulationNo = `CIR-${Date.now()}`;
|
||||
// Generate No. using DocumentNumberingService (Type 900 - Circulation)
|
||||
const circulationNo = await this.numberingService.generateNextNumber({
|
||||
projectId: createDto.projectId || 0, // Use projectId from DTO or 0
|
||||
originatorId: user.primaryOrganizationId,
|
||||
typeId: 900, // Fixed Type ID for Circulation
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
TYPE_CODE: 'CIR',
|
||||
ORG_CODE: 'ORG',
|
||||
},
|
||||
});
|
||||
|
||||
const circulation = queryRunner.manager.create(Circulation, {
|
||||
organizationId: user.primaryOrganizationId,
|
||||
@@ -55,7 +66,7 @@ export class CirculationService {
|
||||
organizationId: user.primaryOrganizationId,
|
||||
assignedTo: userId,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
})
|
||||
);
|
||||
await queryRunner.manager.save(routings);
|
||||
}
|
||||
@@ -83,13 +94,6 @@ export class CirculationService {
|
||||
query.andWhere('c.statusCode = :status', { status });
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query.andWhere(
|
||||
'(c.circulationNo LIKE :search OR c.subject LIKE :search)',
|
||||
{ search: `%${search}%` },
|
||||
);
|
||||
}
|
||||
|
||||
query
|
||||
.orderBy('c.createdAt', 'DESC')
|
||||
.skip((page - 1) * limit)
|
||||
@@ -113,7 +117,7 @@ export class CirculationService {
|
||||
async updateRoutingStatus(
|
||||
routingId: number,
|
||||
dto: UpdateCirculationRoutingDto,
|
||||
user: User,
|
||||
user: User
|
||||
) {
|
||||
const routing = await this.routingRepo.findOne({
|
||||
where: { id: routingId },
|
||||
|
||||
@@ -12,6 +12,10 @@ export class CreateCirculationDto {
|
||||
@IsNotEmpty()
|
||||
correspondenceId!: number; // เอกสารต้นเรื่องที่จะเวียน
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
projectId?: number; // Project ID for Numbering
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
subject!: string; // หัวข้อเรื่อง (Subject)
|
||||
|
||||
@@ -11,31 +11,31 @@ import {
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, Like, In } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { Correspondence } from './entities/correspondence.entity.js';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity.js';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity.js';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity.js';
|
||||
import { RoutingTemplate } from './entities/routing-template.entity.js';
|
||||
import { CorrespondenceRouting } from './entities/correspondence-routing.entity.js';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity.js';
|
||||
import { User } from '../user/entities/user.entity.js';
|
||||
// Entitie
|
||||
import { Correspondence } from './entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
||||
import { RoutingTemplate } from './entities/routing-template.entity';
|
||||
import { CorrespondenceRouting } from './entities/correspondence-routing.entity';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
// DTOs
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js';
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
|
||||
import { AddReferenceDto } from './dto/add-reference.dto.js';
|
||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto.js';
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto';
|
||||
import { AddReferenceDto } from './dto/add-reference.dto';
|
||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto';
|
||||
|
||||
// Interfaces & Enums
|
||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface.js';
|
||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface';
|
||||
|
||||
// Services
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
|
||||
import { JsonSchemaService } from '../json-schema/json-schema.service.js';
|
||||
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { SearchService } from '../search/search.service.js';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
import { JsonSchemaService } from '../json-schema/json-schema.service';
|
||||
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { SearchService } from '../search/search.service';
|
||||
|
||||
@Injectable()
|
||||
export class CorrespondenceService {
|
||||
@@ -62,7 +62,7 @@ export class CorrespondenceService {
|
||||
private workflowEngine: WorkflowEngineService,
|
||||
private userService: UserService,
|
||||
private dataSource: DataSource,
|
||||
private searchService: SearchService,
|
||||
private searchService: SearchService
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateCorrespondenceDto, user: User) {
|
||||
@@ -76,7 +76,7 @@ export class CorrespondenceService {
|
||||
});
|
||||
if (!statusDraft) {
|
||||
throw new InternalServerErrorException(
|
||||
'Status DRAFT not found in Master Data',
|
||||
'Status DRAFT not found in Master Data'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,11 +92,11 @@ export class CorrespondenceService {
|
||||
// Impersonation Logic
|
||||
if (createDto.originatorId && createDto.originatorId !== userOrgId) {
|
||||
const permissions = await this.userService.getUserPermissions(
|
||||
user.user_id,
|
||||
user.user_id
|
||||
);
|
||||
if (!permissions.includes('system.manage_all')) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to create documents on behalf of other organizations.',
|
||||
'You do not have permission to create documents on behalf of other organizations.'
|
||||
);
|
||||
}
|
||||
userOrgId = createDto.originatorId;
|
||||
@@ -104,7 +104,7 @@ export class CorrespondenceService {
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create documents',
|
||||
'User must belong to an organization to create documents'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export class CorrespondenceService {
|
||||
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
|
||||
} catch (error: any) {
|
||||
this.logger.warn(
|
||||
`Schema validation warning for ${type.typeCode}: ${error.message}`,
|
||||
`Schema validation warning for ${type.typeCode}: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export class CorrespondenceService {
|
||||
originatorId: userOrgId,
|
||||
typeId: createDto.typeId,
|
||||
disciplineId: createDto.disciplineId, // ส่ง Discipline (ถ้ามี)
|
||||
subTypeId: createDto.subTypeId, // ส่ง SubType (ถ้ามี)
|
||||
subTypeId: createDto.subTypeId, // ส่ง SubType (ถ้ามี)
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
TYPE_CODE: type.typeCode,
|
||||
@@ -165,6 +165,27 @@ export class CorrespondenceService {
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// [NEW V1.5.1] Start Workflow Instance (After Commit)
|
||||
try {
|
||||
const workflowCode = `CORRESPONDENCE_${type.typeCode}`;
|
||||
await this.workflowEngine.createInstance(
|
||||
workflowCode,
|
||||
'correspondence',
|
||||
savedCorr.id.toString(),
|
||||
{
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
disciplineId: createDto.disciplineId,
|
||||
initiatorId: user.user_id,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Workflow not started for ${docNumber} (Code: CORRESPONDENCE_${type.typeCode}): ${(error as Error).message}`
|
||||
);
|
||||
// Non-blocking: Document is created, but workflow might not be active.
|
||||
}
|
||||
|
||||
this.searchService.indexDocument({
|
||||
id: savedCorr.id,
|
||||
type: 'correspondence',
|
||||
@@ -183,7 +204,7 @@ export class CorrespondenceService {
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Failed to create correspondence: ${(err as Error).message}`,
|
||||
`Failed to create correspondence: ${(err as Error).message}`
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -218,7 +239,7 @@ export class CorrespondenceService {
|
||||
if (search) {
|
||||
query.andWhere(
|
||||
'(corr.correspondenceNumber LIKE :search OR rev.title LIKE :search)',
|
||||
{ search: `%${search}%` },
|
||||
{ search: `%${search}%` }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -268,7 +289,7 @@ export class CorrespondenceService {
|
||||
|
||||
if (!template || !template.steps?.length) {
|
||||
throw new BadRequestException(
|
||||
'Invalid routing template or no steps defined',
|
||||
'Invalid routing template or no steps defined'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -288,7 +309,7 @@ export class CorrespondenceService {
|
||||
stepPurpose: firstStep.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
processedByUserId: user.user_id,
|
||||
processedAt: new Date(),
|
||||
@@ -308,7 +329,7 @@ export class CorrespondenceService {
|
||||
async processAction(
|
||||
correspondenceId: number,
|
||||
dto: WorkflowActionDto,
|
||||
user: User,
|
||||
user: User
|
||||
) {
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id: correspondenceId },
|
||||
@@ -333,19 +354,19 @@ export class CorrespondenceService {
|
||||
|
||||
if (!currentRouting) {
|
||||
throw new BadRequestException(
|
||||
'No active workflow step found for this document',
|
||||
'No active workflow step found for this document'
|
||||
);
|
||||
}
|
||||
|
||||
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'You are not authorized to process this step',
|
||||
'You are not authorized to process this step'
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentRouting.templateId) {
|
||||
throw new InternalServerErrorException(
|
||||
'Routing record missing templateId',
|
||||
'Routing record missing templateId'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -365,7 +386,7 @@ export class CorrespondenceService {
|
||||
currentSeq,
|
||||
totalSteps,
|
||||
dto.action,
|
||||
dto.returnToSequence,
|
||||
dto.returnToSequence
|
||||
);
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
@@ -383,12 +404,12 @@ export class CorrespondenceService {
|
||||
|
||||
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
|
||||
const nextStepConfig = template.steps.find(
|
||||
(s) => s.sequence === result.nextStepSequence,
|
||||
(s) => s.sequence === result.nextStepSequence
|
||||
);
|
||||
|
||||
if (!nextStepConfig) {
|
||||
this.logger.warn(
|
||||
`Next step ${result.nextStepSequence} not found in template`,
|
||||
`Next step ${result.nextStepSequence} not found in template`
|
||||
);
|
||||
} else {
|
||||
const nextRouting = queryRunner.manager.create(
|
||||
@@ -403,9 +424,9 @@ export class CorrespondenceService {
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() +
|
||||
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
},
|
||||
}
|
||||
);
|
||||
await queryRunner.manager.save(nextRouting);
|
||||
}
|
||||
@@ -478,4 +499,4 @@ export class CorrespondenceService {
|
||||
|
||||
return { outgoing, incoming };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ export class Correspondence {
|
||||
@Column({ name: 'correspondence_type_id' })
|
||||
correspondenceTypeId!: number;
|
||||
|
||||
@Column({ name: 'discipline_id', nullable: true })
|
||||
disciplineId?: number;
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
projectId!: number;
|
||||
|
||||
@@ -64,10 +67,15 @@ export class Correspondence {
|
||||
@JoinColumn({ name: 'created_by' })
|
||||
creator?: User;
|
||||
|
||||
// [New V1.5.1]
|
||||
@ManyToOne('Discipline')
|
||||
@JoinColumn({ name: 'discipline_id' })
|
||||
discipline?: any; // Use 'any' or import Discipline entity if available to avoid circular dependency issues if not careful, but better to import.
|
||||
|
||||
// One Correspondence has Many Revisions
|
||||
@OneToMany(
|
||||
() => CorrespondenceRevision,
|
||||
(revision) => revision.correspondence,
|
||||
(revision) => revision.correspondence
|
||||
)
|
||||
revisions?: CorrespondenceRevision[];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
ConflictException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
@@ -38,7 +36,7 @@ export class ShopDrawingService {
|
||||
@InjectRepository(Attachment)
|
||||
private attachmentRepo: Repository<Attachment>,
|
||||
private fileStorageService: FileStorageService,
|
||||
private dataSource: DataSource,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -51,7 +49,7 @@ export class ShopDrawingService {
|
||||
});
|
||||
if (exists) {
|
||||
throw new ConflictException(
|
||||
`Drawing number "${createDto.drawingNumber}" already exists.`,
|
||||
`Drawing number "${createDto.drawingNumber}" already exists.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +101,7 @@ export class ShopDrawingService {
|
||||
// 5. Commit Files
|
||||
if (createDto.attachmentIds?.length) {
|
||||
await this.fileStorageService.commit(
|
||||
createDto.attachmentIds.map(String),
|
||||
createDto.attachmentIds.map(String)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,7 +115,7 @@ export class ShopDrawingService {
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Failed to create shop drawing: ${(err as Error).message}`,
|
||||
`Failed to create shop drawing: ${(err as Error).message}`
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -130,7 +128,7 @@ export class ShopDrawingService {
|
||||
*/
|
||||
async createRevision(
|
||||
shopDrawingId: number,
|
||||
createDto: CreateShopDrawingRevisionDto,
|
||||
createDto: CreateShopDrawingRevisionDto
|
||||
) {
|
||||
const shopDrawing = await this.shopDrawingRepo.findOneBy({
|
||||
id: shopDrawingId,
|
||||
@@ -144,7 +142,7 @@ export class ShopDrawingService {
|
||||
});
|
||||
if (exists) {
|
||||
throw new ConflictException(
|
||||
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`,
|
||||
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,7 +186,7 @@ export class ShopDrawingService {
|
||||
|
||||
if (createDto.attachmentIds?.length) {
|
||||
await this.fileStorageService.commit(
|
||||
createDto.attachmentIds.map(String),
|
||||
createDto.attachmentIds.map(String)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,7 +235,7 @@ export class ShopDrawingService {
|
||||
qb.where('sd.drawingNumber LIKE :search', {
|
||||
search: `%${search}%`,
|
||||
}).orWhere('sd.title LIKE :search', { search: `%${search}%` });
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
70
backend/src/modules/project/contract.controller.ts
Normal file
70
backend/src/modules/project/contract.controller.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiBearerAuth,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { ContractService } from './contract.service.js';
|
||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
@ApiTags('Contracts')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('contracts')
|
||||
export class ContractController {
|
||||
constructor(private readonly contractService: ContractService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Create Contract' })
|
||||
create(@Body() dto: CreateContractDto) {
|
||||
return this.contractService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'Get All Contracts (Optional: filter by projectId)',
|
||||
})
|
||||
@ApiQuery({ name: 'projectId', required: false, type: Number })
|
||||
findAll(@Query('projectId') projectId?: number) {
|
||||
return this.contractService.findAll(projectId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Contract by ID' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.contractService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Update Contract' })
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: UpdateContractDto
|
||||
) {
|
||||
return this.contractService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Delete Contract' })
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.contractService.remove(id);
|
||||
}
|
||||
}
|
||||
65
backend/src/modules/project/contract.service.ts
Normal file
65
backend/src/modules/project/contract.service.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Contract } from './entities/contract.entity.js';
|
||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class ContractService {
|
||||
constructor(
|
||||
@InjectRepository(Contract)
|
||||
private readonly contractRepo: Repository<Contract>
|
||||
) {}
|
||||
|
||||
async create(dto: CreateContractDto) {
|
||||
const existing = await this.contractRepo.findOne({
|
||||
where: { contractCode: dto.contractCode },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Contract Code "${dto.contractCode}" already exists`
|
||||
);
|
||||
}
|
||||
const contract = this.contractRepo.create(dto);
|
||||
return this.contractRepo.save(contract);
|
||||
}
|
||||
|
||||
async findAll(projectId?: number) {
|
||||
const query = this.contractRepo
|
||||
.createQueryBuilder('c')
|
||||
.leftJoinAndSelect('c.project', 'p')
|
||||
.orderBy('c.contractCode', 'ASC');
|
||||
|
||||
if (projectId) {
|
||||
query.where('c.projectId = :projectId', { projectId });
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const contract = await this.contractRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['project'],
|
||||
});
|
||||
if (!contract) throw new NotFoundException(`Contract ID ${id} not found`);
|
||||
return contract;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateContractDto) {
|
||||
const contract = await this.findOne(id);
|
||||
Object.assign(contract, dto);
|
||||
return this.contractRepo.save(contract);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const contract = await this.findOne(id);
|
||||
// Schema doesn't have deleted_at for Contract either.
|
||||
return this.contractRepo.remove(contract);
|
||||
}
|
||||
}
|
||||
49
backend/src/modules/project/dto/create-contract.dto.ts
Normal file
49
backend/src/modules/project/dto/create-contract.dto.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
Length,
|
||||
IsInt,
|
||||
IsDateString,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateContractDto {
|
||||
@ApiProperty({ example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number;
|
||||
|
||||
@ApiProperty({ example: 'C-001' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 50)
|
||||
contractCode!: string;
|
||||
|
||||
@ApiProperty({ example: 'Main Construction Contract' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 255)
|
||||
contractName!: string;
|
||||
|
||||
@ApiProperty({ example: 'Description of the contract', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: '2024-01-01', required: false })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startDate?: string; // Receive as string, TypeORM handles date
|
||||
|
||||
@ApiProperty({ example: '2025-12-31', required: false })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endDate?: string;
|
||||
|
||||
@ApiProperty({ example: true, required: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
27
backend/src/modules/project/dto/create-organization.dto.ts
Normal file
27
backend/src/modules/project/dto/create-organization.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
Length,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateOrganizationDto {
|
||||
@ApiProperty({ example: 'ITD' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 20)
|
||||
organizationCode!: string;
|
||||
|
||||
@ApiProperty({ example: 'Italian-Thai Development' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 255)
|
||||
organizationName!: string;
|
||||
|
||||
@ApiProperty({ example: true, required: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
4
backend/src/modules/project/dto/update-contract.dto.ts
Normal file
4
backend/src/modules/project/dto/update-contract.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateContractDto } from './create-contract.dto.js';
|
||||
|
||||
export class UpdateContractDto extends PartialType(CreateContractDto) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateOrganizationDto } from './create-organization.dto.js';
|
||||
|
||||
export class UpdateOrganizationDto extends PartialType(CreateOrganizationDto) {}
|
||||
61
backend/src/modules/project/organization.controller.ts
Normal file
61
backend/src/modules/project/organization.controller.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { OrganizationService } from './organization.service.js';
|
||||
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
@ApiTags('Organizations')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('organizations')
|
||||
export class OrganizationController {
|
||||
constructor(private readonly orgService: OrganizationService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Create Organization' })
|
||||
create(@Body() dto: CreateOrganizationDto) {
|
||||
return this.orgService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get All Organizations' })
|
||||
findAll() {
|
||||
return this.orgService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Organization by ID' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.orgService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Update Organization' })
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: UpdateOrganizationDto
|
||||
) {
|
||||
return this.orgService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Delete Organization' })
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.orgService.remove(id);
|
||||
}
|
||||
}
|
||||
57
backend/src/modules/project/organization.service.ts
Normal file
57
backend/src/modules/project/organization.service.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationService {
|
||||
constructor(
|
||||
@InjectRepository(Organization)
|
||||
private readonly orgRepo: Repository<Organization>
|
||||
) {}
|
||||
|
||||
async create(dto: CreateOrganizationDto) {
|
||||
const existing = await this.orgRepo.findOne({
|
||||
where: { organizationCode: dto.organizationCode },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Organization Code "${dto.organizationCode}" already exists`
|
||||
);
|
||||
}
|
||||
const org = this.orgRepo.create(dto);
|
||||
return this.orgRepo.save(org);
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.orgRepo.find({
|
||||
order: { organizationCode: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const org = await this.orgRepo.findOne({ where: { id } });
|
||||
if (!org) throw new NotFoundException(`Organization ID ${id} not found`);
|
||||
return org;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateOrganizationDto) {
|
||||
const org = await this.findOne(id);
|
||||
Object.assign(org, dto);
|
||||
return this.orgRepo.save(org);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const org = await this.findOne(id);
|
||||
// Hard delete or Soft delete? Schema doesn't have deleted_at for Organization, but let's check.
|
||||
// Schema says: created_at, updated_at. No deleted_at.
|
||||
// So hard delete.
|
||||
return this.orgRepo.remove(org);
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,32 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ProjectService } from './project.service.js';
|
||||
import { ProjectController } from './project.controller.js';
|
||||
import { OrganizationService } from './organization.service.js';
|
||||
import { OrganizationController } from './organization.controller.js';
|
||||
import { ContractService } from './contract.service.js';
|
||||
import { ContractController } from './contract.controller.js';
|
||||
|
||||
import { Project } from './entities/project.entity.js';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
import { Contract } from './entities/contract.entity.js';
|
||||
import { ProjectOrganization } from './entities/project-organization.entity.js'; // เพิ่ม
|
||||
import { ContractOrganization } from './entities/contract-organization.entity.js'; // เพิ่ม
|
||||
import { ProjectOrganization } from './entities/project-organization.entity.js';
|
||||
import { ContractOrganization } from './entities/contract-organization.entity.js';
|
||||
// Modules
|
||||
import { UserModule } from '../user/user.module'; // ✅ 1. Import UserModule
|
||||
import { UserModule } from '../user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Project,
|
||||
Organization,
|
||||
Contract,
|
||||
ProjectOrganization, // ลงทะเบียน
|
||||
ContractOrganization, // ลงทะเบียน
|
||||
ProjectOrganization,
|
||||
ContractOrganization,
|
||||
]),
|
||||
UserModule, // ✅ 2. เพิ่ม UserModule เข้าไปใน imports
|
||||
UserModule,
|
||||
],
|
||||
controllers: [ProjectController],
|
||||
providers: [ProjectService],
|
||||
exports: [ProjectService], // Export เผื่อ Module อื่นใช้
|
||||
controllers: [ProjectController, OrganizationController, ContractController],
|
||||
providers: [ProjectService, OrganizationService, ContractService],
|
||||
exports: [ProjectService, OrganizationService, ContractService],
|
||||
})
|
||||
export class ProjectModule {}
|
||||
|
||||
@@ -29,6 +29,7 @@ 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';
|
||||
|
||||
// Services
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
@@ -68,7 +69,7 @@ export class RfaService {
|
||||
private workflowEngine: WorkflowEngineService,
|
||||
private notificationService: NotificationService,
|
||||
private dataSource: DataSource,
|
||||
private searchService: SearchService,
|
||||
private searchService: SearchService
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateRfaDto, user: User) {
|
||||
@@ -82,7 +83,7 @@ export class RfaService {
|
||||
});
|
||||
if (!statusDraft) {
|
||||
throw new InternalServerErrorException(
|
||||
'Status DFT (Draft) not found in Master Data',
|
||||
'Status DFT (Draft) not found in Master Data'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,24 +120,20 @@ export class RfaService {
|
||||
// 1. Create Correspondence Record
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber,
|
||||
correspondenceTypeId: createDto.rfaTypeId, // Assuming RFA Type maps directly or via logic
|
||||
// Note: ถ้า CorrespondenceType แยก ID กับ RFA Type ต้อง Map ให้ถูก
|
||||
// ในที่นี้สมมติว่าใช้ ID เดียวกัน หรือ RFA Type เป็น SubType ของ Correspondence
|
||||
correspondenceTypeId: createDto.rfaTypeId,
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
isInternalCommunication: false,
|
||||
isInternal: false,
|
||||
createdBy: user.user_id,
|
||||
// ✅ Add disciplineId column if correspondence table supports it (as per Data Dictionary Update)
|
||||
// disciplineId: createDto.disciplineId
|
||||
disciplineId: createDto.disciplineId, // ✅ Add disciplineId
|
||||
});
|
||||
const savedCorr = await queryRunner.manager.save(correspondence);
|
||||
|
||||
// 2. Create RFA Master Record
|
||||
// 2. Create Rfa Master Record
|
||||
const rfa = queryRunner.manager.create(Rfa, {
|
||||
rfaTypeId: createDto.rfaTypeId,
|
||||
createdBy: user.user_id,
|
||||
// ✅ ถ้า Entity Rfa มี disciplineId ให้ใส่ตรงนี้ด้วย
|
||||
// disciplineId: createDto.disciplineId
|
||||
disciplineId: createDto.disciplineId, // ✅ Add disciplineId
|
||||
});
|
||||
const savedRfa = await queryRunner.manager.save(rfa);
|
||||
|
||||
@@ -154,8 +151,8 @@ export class RfaService {
|
||||
? new Date(createDto.documentDate)
|
||||
: new Date(),
|
||||
createdBy: user.user_id,
|
||||
details: createDto.details, // ✅ Save JSON Details
|
||||
schemaVersion: 1, // ✅ Default Schema Version
|
||||
details: createDto.details,
|
||||
schemaVersion: 1,
|
||||
});
|
||||
const savedRevision = await queryRunner.manager.save(rfaRevision);
|
||||
|
||||
@@ -174,27 +171,48 @@ export class RfaService {
|
||||
|
||||
const rfaItems = shopDrawings.map((sd) =>
|
||||
queryRunner.manager.create(RfaItem, {
|
||||
rfaRevisionId: savedCorr.id, // ใช้ ID ของ Correspondence (ตาม Schema ที่ออกแบบไว้) หรือ RFA Revision ID แล้วแต่การ Map Entity
|
||||
// ตาม Entity RfaItem ที่ให้มา: rfaRevisionId map ไปที่ correspondence_id
|
||||
rfaRevisionId: savedCorr.id, // Use Correspondence ID as per schema
|
||||
shopDrawingRevisionId: sd.id,
|
||||
}),
|
||||
})
|
||||
);
|
||||
await queryRunner.manager.save(rfaItems);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// [NEW V1.5.1] Start Unified Workflow Instance
|
||||
try {
|
||||
const workflowCode = `RFA_${rfaType.typeCode}`; // e.g., RFA_GEN
|
||||
await this.workflowEngine.createInstance(
|
||||
workflowCode,
|
||||
'rfa',
|
||||
savedRfa.id.toString(),
|
||||
{
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
disciplineId: createDto.disciplineId,
|
||||
initiatorId: user.user_id,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Workflow not started for ${docNumber}: ${(error as Error).message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Indexing for Search
|
||||
this.searchService.indexDocument({
|
||||
id: savedCorr.id,
|
||||
type: 'rfa',
|
||||
docNumber: docNumber,
|
||||
title: createDto.title,
|
||||
description: createDto.description,
|
||||
status: 'DRAFT',
|
||||
projectId: createDto.projectId,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
this.searchService
|
||||
.indexDocument({
|
||||
id: savedCorr.id,
|
||||
type: 'rfa',
|
||||
docNumber: docNumber,
|
||||
title: createDto.title,
|
||||
description: createDto.description,
|
||||
status: 'DRAFT',
|
||||
projectId: createDto.projectId,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.catch((err) => this.logger.error(`Indexing failed: ${err}`));
|
||||
|
||||
return {
|
||||
...savedRfa,
|
||||
@@ -284,7 +302,7 @@ export class RfaService {
|
||||
stepPurpose: firstStep.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
processedByUserId: user.user_id,
|
||||
processedAt: new Date(),
|
||||
@@ -293,7 +311,7 @@ export class RfaService {
|
||||
|
||||
// Notify
|
||||
const recipientUserId = await this.userService.findDocControlIdByOrg(
|
||||
firstStep.toOrganizationId,
|
||||
firstStep.toOrganizationId
|
||||
);
|
||||
if (recipientUserId) {
|
||||
await this.notificationService.send({
|
||||
@@ -338,7 +356,7 @@ export class RfaService {
|
||||
throw new BadRequestException('No active workflow step found');
|
||||
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
|
||||
throw new ForbiddenException(
|
||||
'You are not authorized to process this step',
|
||||
'You are not authorized to process this step'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -355,7 +373,7 @@ export class RfaService {
|
||||
currentRouting.sequence,
|
||||
template.steps.length,
|
||||
dto.action,
|
||||
dto.returnToSequence,
|
||||
dto.returnToSequence
|
||||
);
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
@@ -364,16 +382,17 @@ export class RfaService {
|
||||
|
||||
try {
|
||||
// Update current routing
|
||||
currentRouting.status = dto.action === 'REJECT' ? 'REJECTED' : 'ACTIONED';
|
||||
currentRouting.status =
|
||||
dto.action === WorkflowAction.REJECT ? 'REJECTED' : 'ACTIONED';
|
||||
currentRouting.processedByUserId = user.user_id;
|
||||
currentRouting.processedAt = new Date();
|
||||
currentRouting.comments = dto.comments;
|
||||
await queryRunner.manager.save(currentRouting);
|
||||
|
||||
// Create next routing if available
|
||||
if (result.nextStepSequence && dto.action !== 'REJECT') {
|
||||
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
|
||||
const nextStep = template.steps.find(
|
||||
(s) => s.sequence === result.nextStepSequence,
|
||||
(s) => s.sequence === result.nextStepSequence
|
||||
);
|
||||
if (nextStep) {
|
||||
const nextRouting = queryRunner.manager.create(
|
||||
@@ -387,18 +406,20 @@ export class RfaService {
|
||||
stepPurpose: nextStep.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (nextStep.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) {
|
||||
// Workflow Ended (Completed or Rejected)
|
||||
// Update RFA Status (Approved/Rejected Code)
|
||||
if (dto.action !== 'REJECT') {
|
||||
if (dto.action !== WorkflowAction.REJECT) {
|
||||
const approveCode = await this.rfaApproveRepo.findOne({
|
||||
where: { approveCode: dto.action === 'APPROVE' ? '1A' : '4X' },
|
||||
where: {
|
||||
approveCode: dto.action === WorkflowAction.APPROVE ? '1A' : '4X',
|
||||
},
|
||||
}); // Logic Map Code อย่างง่าย
|
||||
if (approveCode) {
|
||||
currentRevision.rfaApproveCodeId = approveCode.id;
|
||||
|
||||
@@ -4,11 +4,12 @@ import {
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
ValidateNested,
|
||||
IsEnum,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
// Enum นี้ควรตรงกับใน Entity หรือสร้างไฟล์ enum แยก (ในที่นี้ใส่ไว้ใน DTO เพื่อความสะดวก)
|
||||
export enum TransmittalPurpose {
|
||||
FOR_APPROVAL = 'FOR_APPROVAL',
|
||||
FOR_INFORMATION = 'FOR_INFORMATION',
|
||||
@@ -16,21 +17,57 @@ export enum TransmittalPurpose {
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export class CreateTransmittalDto {
|
||||
export class TransmittalItemDto {
|
||||
@ApiProperty({
|
||||
description: 'ประเภทรายการ (DRAWING, RFA, CORRESPONDENCE)',
|
||||
example: 'DRAWING',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
itemType!: string;
|
||||
|
||||
@ApiProperty({ description: 'ID ของรายการ', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number; // จำเป็นสำหรับการออกเลขที่เอกสาร (Running Number)
|
||||
|
||||
@IsEnum(TransmittalPurpose)
|
||||
@IsOptional()
|
||||
purpose?: TransmittalPurpose; // วัตถุประสงค์การส่ง
|
||||
itemId!: number;
|
||||
|
||||
@ApiProperty({ description: 'รายละเอียดเพิ่มเติม', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
remarks?: string; // หมายเหตุเพิ่มเติม
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@IsNotEmpty()
|
||||
itemIds!: number[]; // ID ของเอกสาร (Correspondence IDs) ที่จะแนบไปใน Transmittal นี้
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class CreateTransmittalDto {
|
||||
@ApiProperty({ description: 'ID ของโครงการ', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'เรื่อง',
|
||||
example: 'Transmittal for Shop Drawings',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
subject!: string;
|
||||
|
||||
@ApiProperty({ description: 'ผู้รับ (Organization ID)', example: 2 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
recipientOrganizationId!: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'วัตถุประสงค์',
|
||||
enum: TransmittalPurpose,
|
||||
example: TransmittalPurpose.FOR_APPROVAL,
|
||||
})
|
||||
@IsEnum(TransmittalPurpose)
|
||||
@IsOptional()
|
||||
purpose?: TransmittalPurpose;
|
||||
|
||||
@ApiProperty({ description: 'รายการที่แนบ', type: [TransmittalItemDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TransmittalItemDto)
|
||||
items!: TransmittalItemDto[];
|
||||
}
|
||||
|
||||
@@ -1,36 +1,22 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Entity, Column, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
import { Transmittal } from './transmittal.entity';
|
||||
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
||||
|
||||
@Entity('transmittal_items')
|
||||
export class TransmittalItem {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'transmittal_id' })
|
||||
@PrimaryColumn({ name: 'transmittal_id' })
|
||||
transmittalId!: number;
|
||||
|
||||
@Column({ name: 'item_correspondence_id' })
|
||||
itemCorrespondenceId!: number;
|
||||
@PrimaryColumn({ name: 'item_type', length: 50 })
|
||||
itemType!: string; // DRAWING, RFA, etc.
|
||||
|
||||
@Column({ default: 1 })
|
||||
quantity!: number;
|
||||
@PrimaryColumn({ name: 'item_id' })
|
||||
itemId!: number;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
remarks?: string;
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Transmittal, (t) => t.items, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'transmittal_id' })
|
||||
transmittal!: Transmittal;
|
||||
|
||||
@ManyToOne(() => Correspondence)
|
||||
@JoinColumn({ name: 'item_correspondence_id' })
|
||||
itemDocument!: Correspondence;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
||||
import { TransmittalItem } from './transmittal-item.entity';
|
||||
|
||||
@Entity('transmittals')
|
||||
export class Transmittal {
|
||||
@PrimaryColumn({ name: 'correspondence_id' })
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'correspondence_id', unique: true })
|
||||
correspondenceId!: number;
|
||||
|
||||
@Column({ name: 'transmittal_no', length: 100 })
|
||||
transmittalNo!: string;
|
||||
|
||||
@Column({ length: 500 })
|
||||
subject!: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['FOR_APPROVAL', 'FOR_INFORMATION', 'FOR_REVIEW', 'OTHER'],
|
||||
@@ -24,6 +34,9 @@ export class Transmittal {
|
||||
@Column({ type: 'text', nullable: true })
|
||||
remarks?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
// Relations
|
||||
@OneToOne(() => Correspondence)
|
||||
@JoinColumn({ name: 'correspondence_id' })
|
||||
|
||||
@@ -4,50 +4,32 @@ import {
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { TransmittalService } from './transmittal.service';
|
||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
|
||||
import { SearchTransmittalDto } from './dto/search-transmittal.dto'; // เดี๋ยวสร้าง DTO นี้เพิ่มให้ครับถ้ายังไม่มี
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('Transmittals')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('transmittals')
|
||||
export class TransmittalController {
|
||||
constructor(private readonly transmittalService: TransmittalService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create new Transmittal' })
|
||||
@RequirePermission('transmittal.create') // สิทธิ์ ID 40
|
||||
@Audit('transmittal.create', 'transmittal') // ✅ แปะตรงนี้
|
||||
@ApiOperation({ summary: 'Create a new Transmittal' })
|
||||
create(@Body() createDto: CreateTransmittalDto, @CurrentUser() user: User) {
|
||||
return this.transmittalService.create(createDto, user);
|
||||
}
|
||||
|
||||
// เพิ่ม Endpoint พื้นฐานสำหรับการค้นหา (Optional)
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Search Transmittals' })
|
||||
@RequirePermission('document.view')
|
||||
findAll(@Query() searchDto: SearchTransmittalDto) {
|
||||
// return this.transmittalService.findAll(searchDto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Transmittal details' })
|
||||
@RequirePermission('document.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
// return this.transmittalService.findOne(id);
|
||||
return this.transmittalService.findOne(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,23 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Transmittal } from './entities/transmittal.entity';
|
||||
import { TransmittalItem } from './entities/transmittal-item.entity';
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
||||
import { TransmittalService } from './transmittal.service';
|
||||
import { TransmittalController } from './transmittal.controller';
|
||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { SearchModule } from '../search/search.module'; // ✅ ต้อง Import เพราะ Service ใช้ (ที่เป็นสาเหตุ Error)
|
||||
import { SearchModule } from '../search/search.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Transmittal, TransmittalItem, Correspondence]),
|
||||
TypeOrmModule.forFeature([
|
||||
Transmittal,
|
||||
TransmittalItem,
|
||||
Correspondence,
|
||||
CorrespondenceType,
|
||||
CorrespondenceStatus,
|
||||
]),
|
||||
DocumentNumberingModule,
|
||||
UserModule,
|
||||
SearchModule,
|
||||
|
||||
@@ -1,98 +1,143 @@
|
||||
// File: src/modules/transmittal/transmittal.service.ts
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
InternalServerErrorException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, In } from 'typeorm';
|
||||
|
||||
import { Transmittal } from './entities/transmittal.entity.js';
|
||||
import { TransmittalItem } from './entities/transmittal-item.entity.js';
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity.js';
|
||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto.js';
|
||||
import { User } from '../user/entities/user.entity.js';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
|
||||
import { SearchService } from '../search/search.service.js';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { Transmittal } from './entities/transmittal.entity';
|
||||
import { TransmittalItem } from './entities/transmittal-item.entity';
|
||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TransmittalService {
|
||||
private readonly logger = new Logger(TransmittalService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Transmittal)
|
||||
private transmittalRepo: Repository<Transmittal>,
|
||||
@InjectRepository(TransmittalItem)
|
||||
private transmittalItemRepo: Repository<TransmittalItem>,
|
||||
@InjectRepository(Correspondence)
|
||||
private correspondenceRepo: Repository<Correspondence>,
|
||||
private itemRepo: Repository<TransmittalItem>,
|
||||
@InjectRepository(CorrespondenceType)
|
||||
private typeRepo: Repository<CorrespondenceType>,
|
||||
@InjectRepository(CorrespondenceStatus)
|
||||
private statusRepo: Repository<CorrespondenceStatus>,
|
||||
private numberingService: DocumentNumberingService,
|
||||
private dataSource: DataSource,
|
||||
private searchService: SearchService,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateTransmittalDto, user: User) {
|
||||
if (!user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create documents',
|
||||
);
|
||||
}
|
||||
const userOrgId = user.primaryOrganizationId;
|
||||
// 1. Get Transmittal Type (Assuming Code '901' or 'TRN')
|
||||
const type = await this.typeRepo.findOne({
|
||||
where: { typeCode: 'TRN' }, // Adjust code as per Master Data
|
||||
});
|
||||
if (!type) throw new NotFoundException('Transmittal Type (TRN) not found');
|
||||
|
||||
const statusDraft = await this.statusRepo.findOne({
|
||||
where: { statusCode: 'DRAFT' },
|
||||
});
|
||||
if (!statusDraft)
|
||||
throw new InternalServerErrorException('Status DRAFT not found');
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const transmittalTypeId = 3; // TODO: ดึง ID จริงจาก DB หรือ Config
|
||||
const orgCode = 'ORG'; // TODO: Fetch real ORG Code
|
||||
if (!user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create a transmittal'
|
||||
);
|
||||
}
|
||||
|
||||
// [FIXED] เรียกใช้แบบ Object Context
|
||||
try {
|
||||
// 2. Generate Number
|
||||
const docNumber = await this.numberingService.generateNextNumber({
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
typeId: transmittalTypeId,
|
||||
originatorId: user.primaryOrganizationId,
|
||||
typeId: type.id,
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
TYPE_CODE: 'TR',
|
||||
ORG_CODE: orgCode,
|
||||
TYPE_CODE: type.typeCode,
|
||||
ORG_CODE: 'ORG', // TODO: Fetch real ORG Code
|
||||
},
|
||||
});
|
||||
|
||||
// 3. Create Correspondence (Parent)
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber,
|
||||
correspondenceTypeId: transmittalTypeId,
|
||||
correspondenceTypeId: type.id,
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
originatorId: user.primaryOrganizationId,
|
||||
isInternal: false,
|
||||
createdBy: user.user_id,
|
||||
});
|
||||
const savedCorr = await queryRunner.manager.save(correspondence);
|
||||
|
||||
// 4. Create Revision (Draft)
|
||||
const revision = queryRunner.manager.create(CorrespondenceRevision, {
|
||||
correspondenceId: savedCorr.id,
|
||||
revisionNumber: 0,
|
||||
revisionLabel: '0',
|
||||
isCurrent: true,
|
||||
statusId: statusDraft.id,
|
||||
title: createDto.subject,
|
||||
createdBy: user.user_id,
|
||||
});
|
||||
await queryRunner.manager.save(revision);
|
||||
|
||||
// 5. Create Transmittal
|
||||
const transmittal = queryRunner.manager.create(Transmittal, {
|
||||
correspondenceId: savedCorr.id,
|
||||
purpose: createDto.purpose,
|
||||
remarks: createDto.remarks,
|
||||
transmittalNo: docNumber,
|
||||
subject: createDto.subject,
|
||||
});
|
||||
await queryRunner.manager.save(transmittal);
|
||||
const savedTransmittal = await queryRunner.manager.save(transmittal);
|
||||
|
||||
if (createDto.itemIds && createDto.itemIds.length > 0) {
|
||||
const items = createDto.itemIds.map((itemId) =>
|
||||
// 6. Create Items
|
||||
if (createDto.items && createDto.items.length > 0) {
|
||||
const items = createDto.items.map((item) =>
|
||||
queryRunner.manager.create(TransmittalItem, {
|
||||
transmittalId: savedCorr.id,
|
||||
itemCorrespondenceId: itemId,
|
||||
quantity: 1,
|
||||
}),
|
||||
transmittalId: savedTransmittal.id,
|
||||
itemType: item.itemType,
|
||||
itemId: item.itemId,
|
||||
description: item.description,
|
||||
})
|
||||
);
|
||||
await queryRunner.manager.save(items);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { ...savedCorr, transmittal };
|
||||
|
||||
return {
|
||||
...savedTransmittal,
|
||||
correspondence: savedCorr,
|
||||
};
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Failed to create transmittal: ${(err as Error).message}`
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const transmittal = await this.transmittalRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['correspondence', 'items'],
|
||||
});
|
||||
if (!transmittal)
|
||||
throw new NotFoundException(`Transmittal ID ${id} not found`);
|
||||
return transmittal;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user