From dc8b80c5f997749d1176fc8af1ac9ce493108565 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 4 Dec 2025 16:50:09 +0700 Subject: [PATCH] 251204:1700 Prepare to version 1.5.1 --- .prettierrc | 2 +- .vscode/extensions.json | 3 +- .vscode/settings.json | 3 - .../1701676800000-V1_5_1_Schema_Update.ts | 198 + .../modules/circulation/circulation.module.ts | 2 + .../circulation/circulation.service.ts | 34 +- .../circulation/dto/create-circulation.dto.ts | 4 + .../correspondence/correspondence.service.ts | 101 +- .../entities/correspondence.entity.ts | 10 +- .../modules/drawing/shop-drawing.service.ts | 18 +- .../modules/project/contract.controller.ts | 70 + .../src/modules/project/contract.service.ts | 65 + .../project/dto/create-contract.dto.ts | 49 + .../project/dto/create-organization.dto.ts | 27 + .../project/dto/update-contract.dto.ts | 4 + .../project/dto/update-organization.dto.ts | 4 + .../project/organization.controller.ts | 61 + .../modules/project/organization.service.ts | 57 + backend/src/modules/project/project.module.ts | 24 +- backend/src/modules/rfa/rfa.service.ts | 95 +- .../transmittal/dto/create-transmittal.dto.ts | 63 +- .../entities/transmittal-item.entity.ts | 30 +- .../entities/transmittal.entity.ts | 19 +- .../transmittal/transmittal.controller.ts | 28 +- .../modules/transmittal/transmittal.module.ts | 13 +- .../transmittal/transmittal.service.ts | 131 +- docs/0_Requirements_V1_5_1.md | 911 +++++ docs/1_FullStackJS_V1_5_1.md | 1448 +++++++ docs/2_Backend_Plan_V1_5_1.md | 210 + docs/3_Frontend_Plan_V1_5_1.md | 182 + docs/4_Data_Dictionary_V1_5_1.md | 1866 +++++++++ docs/8_lcbp3_v1_5_1.sql | 2391 ++++++------ docs/8_lcbp3_v1_5_1_seed.sql | 3403 +++++++++-------- lcbp3.code-workspace | 99 +- 34 files changed, 8518 insertions(+), 3107 deletions(-) create mode 100644 backend/src/database/migrations/1701676800000-V1_5_1_Schema_Update.ts create mode 100644 backend/src/modules/project/contract.controller.ts create mode 100644 backend/src/modules/project/contract.service.ts create mode 100644 backend/src/modules/project/dto/create-contract.dto.ts create mode 100644 backend/src/modules/project/dto/create-organization.dto.ts create mode 100644 backend/src/modules/project/dto/update-contract.dto.ts create mode 100644 backend/src/modules/project/dto/update-organization.dto.ts create mode 100644 backend/src/modules/project/organization.controller.ts create mode 100644 backend/src/modules/project/organization.service.ts create mode 100644 docs/0_Requirements_V1_5_1.md create mode 100644 docs/1_FullStackJS_V1_5_1.md create mode 100644 docs/2_Backend_Plan_V1_5_1.md create mode 100644 docs/3_Frontend_Plan_V1_5_1.md create mode 100644 docs/4_Data_Dictionary_V1_5_1.md diff --git a/.prettierrc b/.prettierrc index be625ad..82a1e9d 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,7 +4,7 @@ "tabWidth": 2, "useTabs": false, "trailingComma": "es5", - "printWidth": 80, + "printWidth": 120, "arrowParens": "always", "endOfLine": "lf", "bracketSpacing": true, diff --git a/.vscode/extensions.json b/.vscode/extensions.json index aaf647c..d7c1807 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -34,7 +34,6 @@ "wallabyjs.console-ninja", "pkief.material-icon-theme", "github.copilot", - "bierner.markdown-mermaid", - "renesaarsoo.sql-formatter-vsc" + "bierner.markdown-mermaid" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 31c4986..e69de29 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +0,0 @@ -{ - "terminal.integrated.cwd": "\"cwd\": \"D:\\\\nap-dms.lcbp3\\\\frontend\"" -} diff --git a/backend/src/database/migrations/1701676800000-V1_5_1_Schema_Update.ts b/backend/src/database/migrations/1701676800000-V1_5_1_Schema_Update.ts new file mode 100644 index 0000000..37a64f4 --- /dev/null +++ b/backend/src/database/migrations/1701676800000-V1_5_1_Schema_Update.ts @@ -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 { + // 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 { + // 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`); + } +} diff --git a/backend/src/modules/circulation/circulation.module.ts b/backend/src/modules/circulation/circulation.module.ts index bb4637d..bd08197 100644 --- a/backend/src/modules/circulation/circulation.module.ts +++ b/backend/src/modules/circulation/circulation.module.ts @@ -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], diff --git a/backend/src/modules/circulation/circulation.service.ts b/backend/src/modules/circulation/circulation.service.ts index 2b5b935..c274e10 100644 --- a/backend/src/modules/circulation/circulation.service.ts +++ b/backend/src/modules/circulation/circulation.service.ts @@ -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, @InjectRepository(CirculationRouting) private routingRepo: Repository, - 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 }, diff --git a/backend/src/modules/circulation/dto/create-circulation.dto.ts b/backend/src/modules/circulation/dto/create-circulation.dto.ts index b4a754e..ecc2c88 100644 --- a/backend/src/modules/circulation/dto/create-circulation.dto.ts +++ b/backend/src/modules/circulation/dto/create-circulation.dto.ts @@ -12,6 +12,10 @@ export class CreateCirculationDto { @IsNotEmpty() correspondenceId!: number; // เอกสารต้นเรื่องที่จะเวียน + @IsInt() + @IsOptional() + projectId?: number; // Project ID for Numbering + @IsString() @IsNotEmpty() subject!: string; // หัวข้อเรื่อง (Subject) diff --git a/backend/src/modules/correspondence/correspondence.service.ts b/backend/src/modules/correspondence/correspondence.service.ts index 2139e4f..46934df 100644 --- a/backend/src/modules/correspondence/correspondence.service.ts +++ b/backend/src/modules/correspondence/correspondence.service.ts @@ -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 }; } -} \ No newline at end of file +} diff --git a/backend/src/modules/correspondence/entities/correspondence.entity.ts b/backend/src/modules/correspondence/entities/correspondence.entity.ts index 65d9612..b4132b5 100644 --- a/backend/src/modules/correspondence/entities/correspondence.entity.ts +++ b/backend/src/modules/correspondence/entities/correspondence.entity.ts @@ -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[]; } diff --git a/backend/src/modules/drawing/shop-drawing.service.ts b/backend/src/modules/drawing/shop-drawing.service.ts index df7e20c..b6a96fd 100644 --- a/backend/src/modules/drawing/shop-drawing.service.ts +++ b/backend/src/modules/drawing/shop-drawing.service.ts @@ -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, 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}%` }); - }), + }) ); } diff --git a/backend/src/modules/project/contract.controller.ts b/backend/src/modules/project/contract.controller.ts new file mode 100644 index 0000000..1311860 --- /dev/null +++ b/backend/src/modules/project/contract.controller.ts @@ -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); + } +} diff --git a/backend/src/modules/project/contract.service.ts b/backend/src/modules/project/contract.service.ts new file mode 100644 index 0000000..3c9663c --- /dev/null +++ b/backend/src/modules/project/contract.service.ts @@ -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 + ) {} + + 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); + } +} diff --git a/backend/src/modules/project/dto/create-contract.dto.ts b/backend/src/modules/project/dto/create-contract.dto.ts new file mode 100644 index 0000000..dc02112 --- /dev/null +++ b/backend/src/modules/project/dto/create-contract.dto.ts @@ -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; +} diff --git a/backend/src/modules/project/dto/create-organization.dto.ts b/backend/src/modules/project/dto/create-organization.dto.ts new file mode 100644 index 0000000..4f6a8fb --- /dev/null +++ b/backend/src/modules/project/dto/create-organization.dto.ts @@ -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; +} diff --git a/backend/src/modules/project/dto/update-contract.dto.ts b/backend/src/modules/project/dto/update-contract.dto.ts new file mode 100644 index 0000000..54d3091 --- /dev/null +++ b/backend/src/modules/project/dto/update-contract.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateContractDto } from './create-contract.dto.js'; + +export class UpdateContractDto extends PartialType(CreateContractDto) {} diff --git a/backend/src/modules/project/dto/update-organization.dto.ts b/backend/src/modules/project/dto/update-organization.dto.ts new file mode 100644 index 0000000..6c26ad9 --- /dev/null +++ b/backend/src/modules/project/dto/update-organization.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateOrganizationDto } from './create-organization.dto.js'; + +export class UpdateOrganizationDto extends PartialType(CreateOrganizationDto) {} diff --git a/backend/src/modules/project/organization.controller.ts b/backend/src/modules/project/organization.controller.ts new file mode 100644 index 0000000..2cbbaf5 --- /dev/null +++ b/backend/src/modules/project/organization.controller.ts @@ -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); + } +} diff --git a/backend/src/modules/project/organization.service.ts b/backend/src/modules/project/organization.service.ts new file mode 100644 index 0000000..d1fb9e6 --- /dev/null +++ b/backend/src/modules/project/organization.service.ts @@ -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 + ) {} + + 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); + } +} diff --git a/backend/src/modules/project/project.module.ts b/backend/src/modules/project/project.module.ts index af0f422..187c4fb 100644 --- a/backend/src/modules/project/project.module.ts +++ b/backend/src/modules/project/project.module.ts @@ -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 {} diff --git a/backend/src/modules/rfa/rfa.service.ts b/backend/src/modules/rfa/rfa.service.ts index e12ebb1..33b9080 100644 --- a/backend/src/modules/rfa/rfa.service.ts +++ b/backend/src/modules/rfa/rfa.service.ts @@ -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; diff --git a/backend/src/modules/transmittal/dto/create-transmittal.dto.ts b/backend/src/modules/transmittal/dto/create-transmittal.dto.ts index fb1847a..add0783 100644 --- a/backend/src/modules/transmittal/dto/create-transmittal.dto.ts +++ b/backend/src/modules/transmittal/dto/create-transmittal.dto.ts @@ -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[]; } diff --git a/backend/src/modules/transmittal/entities/transmittal-item.entity.ts b/backend/src/modules/transmittal/entities/transmittal-item.entity.ts index 5fb653c..961e50a 100644 --- a/backend/src/modules/transmittal/entities/transmittal-item.entity.ts +++ b/backend/src/modules/transmittal/entities/transmittal-item.entity.ts @@ -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; } diff --git a/backend/src/modules/transmittal/entities/transmittal.entity.ts b/backend/src/modules/transmittal/entities/transmittal.entity.ts index 70b1f76..3c94fdd 100644 --- a/backend/src/modules/transmittal/entities/transmittal.entity.ts +++ b/backend/src/modules/transmittal/entities/transmittal.entity.ts @@ -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' }) diff --git a/backend/src/modules/transmittal/transmittal.controller.ts b/backend/src/modules/transmittal/transmittal.controller.ts index f0a0dbc..f273d1f 100644 --- a/backend/src/modules/transmittal/transmittal.controller.ts +++ b/backend/src/modules/transmittal/transmittal.controller.ts @@ -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); } } diff --git a/backend/src/modules/transmittal/transmittal.module.ts b/backend/src/modules/transmittal/transmittal.module.ts index 123036d..16e1eae 100644 --- a/backend/src/modules/transmittal/transmittal.module.ts +++ b/backend/src/modules/transmittal/transmittal.module.ts @@ -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, diff --git a/backend/src/modules/transmittal/transmittal.service.ts b/backend/src/modules/transmittal/transmittal.service.ts index 325f51e..64bb0bb 100644 --- a/backend/src/modules/transmittal/transmittal.service.ts +++ b/backend/src/modules/transmittal/transmittal.service.ts @@ -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, @InjectRepository(TransmittalItem) - private transmittalItemRepo: Repository, - @InjectRepository(Correspondence) - private correspondenceRepo: Repository, + private itemRepo: Repository, + @InjectRepository(CorrespondenceType) + private typeRepo: Repository, + @InjectRepository(CorrespondenceStatus) + private statusRepo: Repository, 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(); } } -} \ No newline at end of file + + 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; + } +} diff --git a/docs/0_Requirements_V1_5_1.md b/docs/0_Requirements_V1_5_1.md new file mode 100644 index 0000000..02fb7bd --- /dev/null +++ b/docs/0_Requirements_V1_5_1.md @@ -0,0 +1,911 @@ +# 📝 Documents Management System Version 1.5.1: Application Requirements Specification + +**สถานะ:** FINAL-Rev.01 +**วันที่:** 2025-12-04 +**อ้างอิงพื้นฐาน:** v1.5.0 +**Classification:** Internal Technical Documentation + +## 📌 1. Objectives + +# 📌 Section 1: Objectives (วัตถุประสงค์) +--- +title: 'Objectives' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: - +--- + +สร้างเว็บแอปพลิเคชันสำหรับระบบบริหารจัดการเอกสารโครงการ (Document Management System - DMS) แบบครบวงจร ที่เน้นความปลอดภัยสูงสุด ความถูกต้องของข้อมูล (Data Integrity) และรองรับการขยายตัวในอนาคต (Scalability) โดยแก้ไขปัญหา Race Condition และเพิ่มความเสถียรในการจัดการไฟล์ และใช้ Unified Workflow Engine ในการจัดการกระบวนการอนุมัติทั้งหมดเพื่อความยืดหยุ่น + +- มีฟังก์ชันหลักในการอัปโหลด จัดเก็บ ค้นหา แชร์ และควบคุมสิทธิ์การเข้าถึงเอกสาร +- ช่วยลดการใช้เอกสารกระดาษ เพิ่มความปลอดภัยในการจัดเก็บข้อมูล +- เพิ่มความสะดวกในการทำงานร่วมกันระหว่างองค์กร +- ปรับปรุงความปลอดภัยของระบบด้วยมาตรการป้องกันที่ทันสมัย +- เพิ่มความทนทานของระบบด้วยกลไก resilience patterns +- สร้างระบบ monitoring และ observability ที่ครอบคลุม + +## 🛠️ 2. System Architecture + +# 🛠️ Section 2: System Architecture (สถาปัตยกรรมและเทคโนโลยี) +--- +title: 'System Architecture' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: - specs/01-objectives.md +--- + +ชื่อกำหนด สถาปัตยกรรมแบบ Headless/API-First ที่ทันสมัย ทำงานทั้งหมดบน QNAP Server ผ่าน Container Station เพื่อความสะดวกในการจัดการและบำรุงรักษา + +### 2.1 Infrastructure & Environment +- Domain: `np-dms.work`, `www.np-dms.work` +- IP: 159.192.126.103 +- Server: QNAP (Model: TS-473A, RAM: 32GB, CPU: AMD Ryzen V1500B) +- Containerization: Container Station (Docker & Docker Compose) ใช้ UI ของ Container Station เป็นหลัก ในการ configuration และการรัน docker command +- Development Environment: VS Code/Cursor on Windows 11 +- Data Storage: /share/dms-data บน QNAP +- ข้อจำกัด: ไม่สามารถใช้ .env ในการกำหนดตัวแปรภายนอกได้ ต้องกำหนดใน docker-compose.yml เท่านั้น + +### 2.2 Configuration Management +- ใช้ docker-compose.yml สำหรับ environment variables ตามข้อจำกัดของ QNAP +- Secrets Management: ใช้ docker-compose.override.yml (gitignore) สำหรับ secret injection, Docker secrets หรือ Hashicorp Vault, encrypted env vars +- Development environment ยังใช้ .env ได้ แต่ต้องไม่ commit เข้า version control +- มี configuration validation during application startup +- แยก configuration ตาม environment (development, staging, production) +- Docker Network: lcbp3 + +### 2.3 Core Services +- Code Hosting: Gitea (`git.np-dms.work`) +- Backend / Data Platform: NestJS (`backend.np-dms.work`) +- Database: MariaDB 10.11 (`db.np-dms.work`) +- Database Management UI: phpMyAdmin (`pma.np-dms.work`) +- Frontend: Next.js (`lcbp3.np-dms.work`) +- Workflow Automation: n8n (`n8n.np-dms.work`) +- Reverse Proxy: Nginx Proxy Manager (`npm.np-dms.work`) +- Search Engine: Elasticsearch +- Cache: Redis + +### 2.4 Business Logic & Consistency +- Unified Workflow Engine (central) with DSL JSON configuration +- Versioning of workflow definitions, optimistic locking with Redis lock for document numbering +- No SQL triggers; all business logic in NestJS services + +## 📦 3. Functional Requirements + +### 3.1 Project Management + +# 3.1 Project Management (การจัดการโครงสร้างโครงการและองค์กร) +--- +title: "Functional Requirements: Project Management" +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: - +--- +- 3.1.1. โครงการ (Projects): ระบบต้องสามารถจัดการเอกสารภายในหลายโครงการได้ (ปัจจุบันมี 4 โครงการ และจะเพิ่มขึ้นในอนาคต) +- 3.1.2. สัญญา (Contracts): ระบบต้องสามารถจัดการเอกสารภายในแต่ละสัญญาได้ ในแต่ละโครงการ มีได้หลายสัญญา หรืออย่างน้อย 1 สัญญา +- 3.1.3. องค์กร (Organizations): + - มีหลายองค์กรในโครงการ Owner, Designer, Consultant สามารถอยู่หลายโครงการและสัญญาได้ + - Contractor จะถือ 1 สัญญา และอยู่ใน 1 โครงการเท่านั้น + +### 3.2 Correspondence Management + +# 3.2 การจัดการเอกสารโต้ตอบ (Correspondence Management) +--- +title: 'Functional Requirements: Correspondence Management' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: - +--- +- 3.2.1. วัตถุประสงค์: เอกสารโต้ตอบระหว่างองค์กรภายในและภายนอกโครงการ, รองรับ To และ CC หลายองค์กร +- 3.2.2. ประเภทเอกสาร: PDF, ZIP; Types include Letter, Email, RFI, RFA (with revisions) +- 3.2.3. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์สร้าง Draft, Submit requires Admin approval +- 3.2.4. การอ้างอิงและจัดกลุ่ม: รองรับหลาย Reference, Tagging +- 3.2.5. Workflow: รองรับ Unified Workflow + +### 3.3 RFA Management + +# 3.3 RFA Management (การจัดการเอกสาขออนุมัติ) +--- +title: 'Functional Requirements: RFA Management' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: - +--- +- 3.3.1. วัตถุประสงค์: เอกสารขออนุมัติภายในโครงการ +- 3.3.2. ประเภทเอกสาร: PDF, รองรับหลาย revision และหลายประเภท RFA +- 3.3.3. การสร้างเอกสาร: Draft creation by Document Control, Submit requires Admin +- 3.3.4. การอ้างอิง: สามารถอ้างถึง Shop Drawing ได้หลายฉบับ +- 3.3.5. Workflow: รองรับ Unified Workflow + +### 3.4 Contract Drawing Management + +# 3.4 Contract Drawing Management (การจัดการแบบคู่สัญญา) +--- +title: 'Functional Requirements: Contract Drawing Management' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: - +--- +- 3.4.1. วัตถุประสงค์: ใช้เพื่ออ้างอิงและตรวจสอบ +- 3.4.2. ประเภทเอกสาร: PDF +- 3.4.3. การสร้างเอกสาร: ผู้มีสิทธิ์สร้างและแก้ไข +- 3.4.4. การอ้างอิง: ใช้สำหรับอ้างอิงใน Shop Drawings + +### 3.5 Shop Drawing Management + +# 3.5 Shop Drawing Management (การจัดการแบบก่อสร้าง) +--- +title: 'Functional Requirements: Shop Drawing Management' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: - +--- +- 3.5.1. วัตถุประสงค์: ใช้ในการตรวจสอบและจัดส่งด้วย RFA +- 3.5.2. ประเภทเอกสาร: PDF, DWG, ZIP +- 3.5.3. การสร้างเอกสาร: ผู้มีสิทธิ์สร้าง/แก้ไข, Draft visibility control +- 3.5.4. การอ้างอิง: ใช้ใน RFA, มีการจัดหมวดหมู่, แต่ละ revision มี RFA หนึ่งฉบับ + +### 3.6 Unified Workflow Management + +# 3.6 Unified Workflow Management (การจัดการ Workflow) +--- +title: 'Functional Requirements: Unified Workflow Management' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: - +--- +- 3.6.1 Workflow Definition: Admin can create/edit rules via UI DSL Editor, define State, Transition, Role, Condition +- 3.6.2 Workflow Execution: Create instances polymorphic to documents, support actions Approve, Reject, Comment, Return, auto-actions +- 3.6.3 Flexibility: Parallel Review, Conditional Flow +- 3.6.4 Approval Flow: Supports complex multi-organization sequences and return paths +- 3.6.5 Management: Deadline setting, notifications, step skipping, backtrack + +# 3.7 Transmittals Management (การจัดการเอกสารนำส่ง) +--- +title: 'Functional Requirements: Transmittals Management' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: + - specs/01-requirements/01-objectives.md + - specs/01-requirements/02-architecture.md + - specs/01-requirements/03-functional-requirements.md +--- +## 3.7.1. วัตถุประสงค์: +- เอกสารนำส่ง ใช้สำหรับ นำส่ง Request for Approval (RFAS) หลายฉบับ ไปยังองค์กรอื่น + +## 3.7.2. ประเภทเอกสาร: +- ไฟล์ PDF + +## 3.7.3. การสร้างเอกสาร: +- ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้ + +## 3.7.4. การอ้างอิงและจัดกลุ่ม: +- เอกสารนำส่ง เป็นส่วนหนึ่งใน Correspondence + +# 3.8 Circulation Sheet Management (การจัดการใบเวียนเอกสาร) +--- +title: 'Functional Requirements: Circulation Sheet Management' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: + - specs/01-requirements/01-objectives.md + - specs/01-requirements/02-architecture.md + - specs/01-requirements/03-functional-requirements.md +--- +## 3.8.1. วัตถุประสงค์: +- การสื่อสาร เอกสาร (Correspondence) ทุกฉบับ จะมีใบเวียนเอกสารเพื่อควบคุมและมอบหมายงานภายในองค์กร (สามารถดูและแก้ไขได้เฉพาะคนในองค์กร) + +## 3.8.2. ประเภทเอกสาร: +- ไฟล์ PDF + +## 3.8.3. การสร้างเอกสาร: +- ผู้ใช้ที่มีสิทธิ์ในองค์กรนั้น สามารถสร้างและแก้ไขได้ + +## 3.8.4. การอ้างอิงและจัดกลุ่ม: +- การระบุผู้รับผิดชอบ: + - ผู้รับผิดชอบหลัก (Main): มีได้หลายคน + - ผู้ร่วมปฏิบัติงาน (Action): มีได้หลายคน + - ผู้ที่ต้องรับทราบ (Information): มีได้หลายคน + +## 3.8.5. การติดตามงาน: +- สามารถกำหนดวันแล้วเสร็จ (Deadline) สำหรับผู้รับผิดชอบประเภท Main และ Action ได้ +- มีระบบแจ้งเตือนเมื่อมี Circulation ใหม่ และแจ้งเตือนล่วงหน้าก่อนถึงวันแล้วเสร็จ +- สามารถปิด Circulation ได้เมื่อดำเนินการตอบกลับไปยังองค์กรผู้ส่ง (Originator) แล้ว หรือ รับทราบแล้ว (For Information) + +# 3.9 Logs Management (ประวัติการแก้ไข) +--- +title: 'Functional Requirements: Logs Management' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: + - specs/01-requirements/01-objectives.md + - specs/01-requirements/02-architecture.md + - specs/01-requirements/03-functional-requirements.md +--- +## 3.9.1. วัตถุประสงค์: +- เพื่อ บันทึกการกระทำ CRUD ของเอกสารทั้งหมด รวมถึงการ เข้าใช้งาน ของ users +- admin สามารถดูประวัติการแก้ไขของเอกสารทั้งหมด พร้อม จัดทำรายงายตามข้อกำหนดที่ ต้องการได้ + +# 3.10 File Handling Management (การจัดการไฟล์) +--- +title: 'Functional Requirements: File Handling Management' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: + - specs/01-requirements/01-objectives.md + - specs/01-requirements/02-architecture.md + - specs/01-requirements/03-functional-requirements.md +--- +## 3.10.1 Two-Phase Storage Strategy: +1. Phase 1 (Upload): ไฟล์ถูกอัปโหลดเข้าโฟลเดอร์ temp/ และได้รับ temp_id +2. Phase 2 (Commit): เมื่อ User กด Submit ฟอร์มสำเร็จ ระบบจะย้ายไฟล์จาก temp/ ไปยัง permanent/{YYYY}/{MM}/ และบันทึกลง Database ภายใน Transaction เดียวกัน +3. Cleanup: มี Cron Job ลบไฟล์ใน temp/ ที่ค้างเกิน 24 ชม. (Orphan Files) + +## 3.10.2 Security: +- Virus Scan (ClamAV) ก่อนย้ายเข้า permanent +- Whitelist File Types: PDF, DWG, DOCX, XLSX, ZIP +- Max Size: 50MB +- Access Control: ตรวจสอบสิทธิ์ผ่าน Junction Table ก่อนให้ Download Link + +## 3.10.3 ความปลอดภัยของการจัดเก็บไฟล์: +- ต้องมีการ scan virus สำหรับไฟล์ที่อัปโหลดทั้งหมด โดยใช้ ClamAV หรือบริการ third-party +- จำกัดประเภทไฟล์ที่อนุญาต: PDF, DWG, DOCX, XLSX, ZIP (ต้องระบุรายการที่ชัดเจน) +- ขนาดไฟล์สูงสุด: 50MB ต่อไฟล์ +- ไฟล์ต้องถูกเก็บนอก web root และเข้าถึงได้ผ่าน authenticated endpoint เท่านั้น +- ต้องมี file integrity check (checksum) เพื่อป้องกันการแก้ไขไฟล์ +- Download links ต้องมี expiration time (default: 24 ชั่วโมง) +- ต้องบันทึก audit log ทุกครั้งที่มีการดาวน์โหลดไฟล์สำคัญ + +# 3.11 Document Numbering Management (การจัดการเลขที่เอกสาร) +--- +title: 'Functional Requirements: Document Numbering Management' +version: 1.6.0 +status: draft +owner: Nattanin Peancharoen +last_updated: 2025-12-02 +related: + - specs/01-requirements/01-objectives.md + - specs/01-requirements/02-architecture.md + - specs/01-requirements/03-functional-requirements.md + - specs/03-implementation/document-numbering.md + - specs/04-operations/document-numbering-operations.md + - specs/04-data-dictionary/4_Data_Dictionary_V1_4_4.md +--- +## 3.11.1 วัตถุประสงค์: +- ระบบต้องสามารถสร้างเลขที่เอกสาร (Running Number) ได้โดยอัตโนมัติและยืดหยุ่นสูง +- ระบบต้องสามารถกำหนดรูปแบบ (template) เลขที่เอกสารได้ สำหรับแต่ละโครงการ, ชนิดเอกสาร, ประเภทเอกสาร +- ระบบต้องรับประกัน Uniqueness ของเลขที่เอกสารในทุกสถานการณ์ +- ระบบต้องรองรับการทำงานแบบ concurrent ได้อย่างปลอดภัย + +## 3.11.2 Logic การนับเลข (Counter Logic) +การนับเลขจะแยกตาม **Counter Key** ที่ประกอบด้วยหลายส่วน ขึ้นกับประเภทเอกสาร + +### Counter Key Components +| Component | Required? | Description | Database Source | Default if NULL | +| ---------------------------- | ---------------- | ------------------- | --------------------------------------------------------- | --------------- | +| `project_id` | ✅ Yes | ID โครงการ | Derived from user context or organization | - | +| `originator_organization_id` | ✅ Yes | ID องค์กรผู้ส่ง | `correspondences.originator_id` | - | +| `recipient_organization_id` | Depends on type | ID องค์กรผู้รับหลัก (TO) | `correspondence_recipients` where `recipient_type = 'TO'` | NULL for RFA | +| `correspondence_type_id` | ✅ Yes | ID ประเภทเอกสาร | `correspondence_types.id` | - | +| `sub_type_id` | TRANSMITTAL only | ID ประเภทย่อย | `correspondence_sub_types.id` | 0 | +| `rfa_type_id` | RFA only | ID ประเภท RFA | `rfa_types.id` | 0 | +| `discipline_id` | RFA only | ID สาขางาน | `disciplines.id` | 0 | +| `current_year` | ✅ Yes | ปี ค.ศ. | System year (ปัจจุบัน) | - | + +### Counter Key แยกตามประเภทเอกสาร +**LETTER / RFI / MEMO / EMAIL / MOM / INSTRUCTION / NOTICE / OTHER**: +``` +(project_id, originator_organization_id, recipient_organization_id, + correspondence_type_id, 0, 0, 0, current_year) +``` +*หมายเหตุ*: ไม่ใช้ `discipline_id`, `sub_type_id`, `rfa_type_id` + +**TRANSMITTAL**: +``` +(project_id, originator_organization_id, recipient_organization_id, + correspondence_type_id, sub_type_id, 0, 0, current_year) +``` +*หมายเหตุ*: ใช้ `sub_type_id` เพิ่มเติม + +**RFA**: +``` +(project_id, originator_organization_id, NULL, + correspondence_type_id, 0, rfa_type_id, discipline_id, current_year) +``` +*หมายเหตุ*: RFA ไม่ใช้ `recipient_organization_id` เพราะเป็นเอกสารโครงการ (CONTRACTOR → CONSULTANT → OWNER) + +### วิธีการหา project_id +1. **User Context** (แนะนำ): + - เมื่อ User สร้างเอกสาร UI จะให้เลือก Project/Contract ก่อน + - ใช้ `project_id` จาก Context ที่เลือก +2. **จาก Organization**: + - Query `project_organizations` หรือ `contract_organizations` + - ใช้ `originator_organization_id` หา project ที่เกี่ยวข้อง + - ถ้ามีหลาย project ให้ User เลือก +3. **Validation**: + - ตรวจสอบว่า organization มีสิทธิ์ใน project นั้น + - ตรวจสอบว่า project/contract เป็น active + +### Fallback สำหรับค่า NULL +- `discipline_id`: ใช้ `0` (ไม่ระบุสาขางาน) +- `sub_type_id`: ใช้ `0` (ไม่มีประเภทย่อย) +- `rfa_type_id`: ใช้ `0` (ไม่ระบุประเภท RFA) +- `recipient_organization_id`: ใช้ `NULL` สำหรับ RFA, Required สำหรับ LETTER/TRANSMITTAL + +## 3.11.3 Format Templates by Correspondence Type +### 3.11.3.1. Letter (TYPE = LETTER) +**Template**: +``` +{ORIGINATOR}-{RECIPIENT}-{SEQ:4}-{YEAR:B.E.} +``` +**Example**: `คคง.-สคฉ.3-0001-2568` +**Token Breakdown**: +- `คคง.` = {ORIGINATOR} = รหัสองค์กรผู้ส่ง +- `สคฉ.3` = {RECIPIENT} = รหัสองค์กรผู้รับหลัก (TO) +- `0001` = {SEQ:4} = Running number (เริ่ม 0001, 0002, ...) +- `2568` = {YEAR:B.E.} = ปี พ.ศ. +> **⚠️ Template vs Counter Separation** +- {CORR_TYPE} **ไม่แสดง**ใน template เพื่อความกระชับ +- แต่ระบบ**ยังใช้ correspondence_type_id ใน Counter Key** เพื่อแยก counter +- LETTER, MEMO, RFI **มี counter แยกกัน** แม้ template format เหมือนกัน +**Counter Key**: `(project_id, originator_org_id, recipient_org_id, corr_type_id, 0, 0, 0, year)` +--- +### 3.11.3.2. Transmittal (TYPE = TRANSMITTAL) +**Template**: +``` +{ORIGINATOR}-{RECIPIENT}-{SUB_TYPE}-{SEQ:4}-{YEAR:B.E.} +``` +**Example**: `คคง.-สคฉ.3-21-0117-2568` +**Token Breakdown**: +- `คคง.` = {ORIGINATOR} +- `สคฉ.3` = {RECIPIENT} +- `21` = {SUB_TYPE} = หมายเลขประเภทย่อย (11=MAT, 12=SHP, 13=DWG, 14=MET, ...) +- `0117` = {SEQ:4} +- `2568` = {YEAR:B.E.} +> **⚠️ Template vs Counter Separation** +- {CORR_TYPE} **ไม่แสดง**ใน template (เหมือน LETTER) +- TRANSMITTAL มี counter แยกจาก LETTER +**Counter Key**: `(project_id, originator_org_id, recipient_org_id, corr_type_id, sub_type_id, 0, 0, year)` +--- +### 3.11.3.3. RFA (Request for Approval) +**Template**: +``` +{PROJECT}-{CORR_TYPE}-{DISCIPLINE}-{RFA_TYPE}-{SEQ:4}-{REV} +``` +**Example**: `LCBP3-C2-RFA-TER-RPT-0001-A` +**Token Breakdown**: +- `LCBP3-C2` = {PROJECT} = รหัสโครงการ +- `RFA` = {CORR_TYPE} = ประเภทเอกสาร (แสดงใน RFA template) +- `TER` = {DISCIPLINE} = รหัสสาขางาน (TER=Terminal, STR=Structure, ...) +- `RPT` = {RFA_TYPE} = ประเภท RFA (RPT=Report, SDW=Shop Drawing, ...) +- `0001` = {SEQ:4} +- `A` = {REV} = Revision code +> **📋 RFA Workflow** +- RFA เป็น **เอกสารโครงการ** (Project-level document) +- Workflow: **CONTRACTOR → CONSULTANT → OWNER** +- ไม่มี specific `recipient_id` เพราะเป็น workflow ที่กำหนดไว้แล้ว +**Counter Key**: `(project_id, originator_org_id, NULL, corr_type_id, 0, rfa_type_id, discipline_id, year)` +--- +## 3.11.4. Security & Data Integrity Requirements +### 3.11.4.1. Concurrency Control +**Requirements:** +- ระบบ**ต้อง**ป้องกัน race condition เมื่อมีการสร้างเลขที่เอกสารพร้อมกัน +- ระบบ**ต้อง**รับประกัน uniqueness ของเลขที่เอกสารในทุกสถานการณ์ +- ระบบ**ควร**ใช้ Distributed Lock (Redis) เป็นกลไก primary +- ระบบ**ต้อง**มี fallback mechanism เมื่อ Redis ไม่พร้อมใช้งาน +### 3.11.4.2. Data Integrity +**Requirements:** +- ระบบ**ต้อง**ใช้ Optimistic Locking เพื่อตรวจจับ concurrent updates +- ระบบ**ต้อง**มี database constraints เพื่อป้องกันข้อมูลผิดพลาด: + - Unique constraint บน `document_number` + - Foreign key constraints ทุก relationship + - Check constraints สำหรับ business rules +--- +## 3.11.5. Validation Rules +- ต้องมี JSON schema validation สำหรับแต่ละประเภท +- ต้องรองรับ versioning ของ schema +- ต้องมี default values สำหรับ field ที่ไม่บังคับ +- ต้องตรวจสอบ data types และ format ให้ถูกต้อง +--- +## 3.11.6. Performance Requirements +- JSON field ต้องมีขนาดไม่เกิน 50KB +- ต้องรองรับ indexing สำหรับ field ที่ใช้ค้นหาบ่อย +- ต้องมี compression สำหรับ JSON ขนาดใหญ่ +--- +## 3.11.7. Security Requirements +- ต้อง sanitize JSON input เพื่อป้องกัน injection attacks +- ต้อง validate JSON structure ก่อนบันทึก +- ต้อง encrypt sensitive data ใน JSON fields +--- +## 3.11.8. JSON Schema Migration Strategy +- สำหรับ Schema Breaking Changes: + - Phase 1 - Add New Column + ALTER TABLE correspondence_revisions + ADD COLUMN ref_project_id_v2 INT GENERATED ALWAYS AS + (JSON_UNQUOTE(JSON_EXTRACT(details, '$.newProjectIdPath'))) VIRTUAL; + - Phase 2 - Backfill Old Records + - ใช้ background job แปลง JSON format เก่าเป็นใหม่ + - Update `details` JSON ทีละ batch (1000 records) + - Phase 3 - Switch Application Code + - Deploy code ที่ใช้ path ใหม่ + - Phase 4 - Remove Old Column + - หลังจาก verify แล้วว่าไม่มี error + - Drop old virtual column +- สำหรับ Non-Breaking Changes + - เพิ่ม optional field ใน schema + - Old records ที่ไม่มี field = ใช้ default value +--- +# 3.12 JSON Details Management (การจัดการ JSON Details) +--- +title: 'Functional Requirements: JSON Details Management' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: + - specs/01-requirements/01-objectives.md + - specs/01-requirements/02-architecture.md + - specs/01-requirements/03-functional-requirements.md +--- +## 3.12.1 วัตถุประสงค์ +- จัดเก็บข้อมูลแบบไดนามิกที่เฉพาะเจาะจงกับแต่ละประเภทของเอกสาร +- รองรับการขยายตัวของระบบโดยไม่ต้องเปลี่ยนแปลง database schema +- จัดการ metadata และข้อมูลประกอบสำหรับ correspondence, routing, และ workflows +## 3.12.2 โครงสร้าง JSON Schema +- ระบบต้องมี predefined JSON schemas สำหรับประเภทเอกสารต่างๆ: + - 3.12.2.1 Correspondence Types + - GENERIC: ข้อมูลพื้นฐานสำหรับเอกสารทั่วไป + - RFI: รายละเอียดคำถามและข้อมูลทางเทคนิค + - RFA: ข้อมูลการขออนุมัติแบบและวัสดุ + - TRANSMITTAL: รายการเอกสารที่ส่งต่อ + - LETTER: ข้อมูลจดหมายทางการ + - EMAIL: ข้อมูลอีเมล + - 3.12.2.2 Rworkflow Types + - workflow_definitions: กฎและเงื่อนไขการส่งต่อ + - workflow_histories: สถานะและประวัติการส่งต่อ + - workflow_instances: การดำเนินการในแต่ละขั้นตอน + - 3.12.2.3 Audit Types + - AUDIT_LOG: ข้อมูลการตรวจสอบ + - SECURITY_SCAN: ผลการตรวจสอบความปลอดภัย +## 3.12.3 Virtual Columns (ปรับปรุง) +- สำหรับ Field ใน JSON ที่ต้องใช้ในการค้นหา (Search) หรือจัดเรียง (Sort) บ่อยๆ ต้องสร้าง Generated Column (Virtual Column) ใน Database และทำ Index ไว้ เพื่อประสิทธิภาพสูงสุด +- Schema Consistency: Field ที่ถูกกำหนดเป็น Virtual Column ห้าม เปลี่ยนแปลง Key Name หรือ Data Type ใน JSON Schema Version ถัดไป หากจำเป็นต้องเปลี่ยน ต้องมีแผนการ Re-index หรือ Migration ข้อมูลเดิมที่ชัดเจน +## 3.12.4 Validation Rules +- ต้องมี JSON schema validation สำหรับแต่ละประเภท +- ต้องรองรับ versioning ของ schema +- ต้องมี default values สำหรับ field ที่ไม่บังคับ +- ต้องตรวจสอบ data types และ format ให้ถูกต้อง +## 3.12.5 Performance Requirements +- JSON field ต้องมีขนาดไม่เกิน 50KB +- ต้องรองรับ indexing สำหรับ field ที่ใช้ค้นหาบ่อย +- ต้องมี compression สำหรับ JSON ขนาดใหญ่ +## 3.12.6 Security Requirements +- ต้อง sanitize JSON input เพื่อป้องกัน injection attacks +- ต้อง validate JSON structure ก่อนบันทึก +- ต้อง encrypt sensitive data ใน JSON fields +--- + +## 📂 4. Non‑Functional Requirements + +# 4.1 Access Control +# 🔐 Section 4: Access Control (ข้อกำหนดด้านสิทธิ์และการเข้าถึง) + +## 4.1. Overview: + +- Users and organizations can view and edit documents based on the permissions they have. The system's permissions will be based on Role-Based Access Control (RBAC). + +## 4.2. Permission Hierarchy: + +- Global: The highest level of permissions in the system +- Organization: Permissions within an organization, which is the basic permission for users +- Project: Permissions specific to a project, which will be considered when the user is in that project +- Contract: Permissions specific to a contract, which will be considered when the user is in that contract + +## 4.3. Permission Enforcement: + +- When checking permissions, the system will consider permissions from all levels that the user has and use the most permissive permission as the decision +- Example: User A is a Viewer in the organization, but is assigned as an Editor in Project X when in Project X, User A will have the right to edit + +## 4.4. Role and Scope: + +| Role | Scope | Description | Key Permissions | +| :------------------- | :----------- | :------------------------- | :-------------------------------------------------------------------------------------------------------------------- | +| **Superadmin** | Global | System administrator | Do everything in the system, manage organizations, manage global data | +| **Org Admin** | Organization | Organization administrator | Manage users in the organization, manage roles/permissions within the organization, view organization reports | +| **Document Control** | Organization | Document controller | Add/edit/delete documents, set document permissions within the organization | +| **Editor** | Organization | Document editor | Edit documents that have been assigned to them | +| **Viewer** | Organization | Document viewer | View documents that have access permissions | +| **Project Manager** | Project | Project manager | Manage members in the project (add/delete/assign roles), create/manage contracts in the project, view project reports | +| **Contract Admin** | Contract | Contract administrator | Manage users in the contract, manage roles/permissions within the contract, view contract reports | + +## 4.5. Token Management (ปรับปรุง) + +- **Payload Optimization:** ใน JWT Access Token ให้เก็บเฉพาะ `userId` และ `scope` ปัจจุบันเท่านั้น +- **Permission Caching:** สิทธิ์ละเอียด (Permissions List) ให้เก็บใน **Redis** และดึงมาตรวจสอบเมื่อ Request เข้ามา เพื่อลดขนาด Token และเพิ่มความเร็ว + +## 4.6. Onboarding Workflow + +- 4.6.1. Create Organization + - **Superadmin** creates a new organization (e.g. Company A) + - **Superadmin** appoints at least 1 user as **Org Admin** or **Document Control** of Company A +- 4.6.2. Add Users to Organization + - **Org Admin** of Company A adds other users (Editor, Viewer) to the organization +- 4.6.3. Assign Users to Project + - **Project Manager** of Project X (which may come from Company A or another company) invites or assigns users from different organizations to join Project X + - In this step, **Project Manager** will assign **Project Role** (e.g. Project Member, or may use organization-level permissions) +- 4.6.4. Assign Users to Contract + - **Contract Admin** of Contract Y (which is part of Project X) selects users from Project X and assigns them to Contract Y + - In this step, **Contract Admin** will assign **Contract Role** (e.g. Contract Member) and specific permissions +- 4.6.5 Security Onboarding: + - Force users to change password for the first time + - Security awareness training for users with high permissions + - Safe password reset process + - Audit log recording every permission change + +### **4.7. Master Data Management** + +| Master Data | Manager | Scope | +| :-------------------------------------- | :------------------------------ | :------------------------------ | +| Document Type (Correspondence, RFA) | **Superadmin** | Global | +| Document Status (Draft, Approved, etc.) | **Superadmin** | Global | +| Shop Drawing Category | **Project Manager** | Project (สร้างใหม่ได้ภายในโครงการ) | +| Tags | **Org Admin / Project Manager** | Organization / Project | +| Custom Roles | **Superadmin / Org Admin** | Global / Organization | +| Document Numbering Formats | **Superadmin / Admin** | Global / Organization | + +## 4.8. การบันทึกการกระทำ (Audit Log) + +- ทุกการกระทำที่สำคัญของผู้ใช้ (สร้าง, แก้ไข, ลบ, ส่ง) จะถูกบันทึกไว้ใน audit_logs เพื่อการตรวจสอบย้อนหลัง + - ขอบเขตการบันทึก Audit Log: + - ทุกการสร้าง/แก้ไข/ลบ ข้อมูลสำคัญ (correspondences, RFAs, drawings, users, permissions) + - ทุกการเข้าถึงข้อมูล sensitive (user data, financial information) + - ทุกการเปลี่ยนสถานะ workflow (status transitions) + - ทุกการดาวน์โหลดไฟล์สำคัญ (contract documents, financial reports) + - ทุกการเปลี่ยนแปลง permission และ role assignment + - ทุกการล็อกอินที่สำเร็จและล้มเหลว + - ทุกการส่งคำขอ API ที่สำคัญ + - ข้อมูลที่ต้องบันทึกใน Audit Log: + - ผู้ใช้งาน (user_id) + - การกระทำ (action) + - ชนิดของ entity (entity_type) + - ID ของ entity (entity_id) + - ข้อมูลก่อนการเปลี่ยนแปลง (old_values) - สำหรับ update operations + - ข้อมูลหลังการเปลี่ยนแปลง (new_values) - สำหรับ update operations + - IP address + - User agent + - Timestamp + - Request ID สำหรับ tracing + +## 4.9. Data Archiving & Partitioning + +- สำหรับตารางที่มีขนาดใหญ่และโตเร็ว (เช่น `audit_logs`, `notifications`, `correspondence_revisions`) ต้องออกแบบโดยรองรับ **Table Partitioning** (แบ่งตาม Range วันที่ หรือ List) เพื่อประสิทธิภาพในระยะยาว + +## 4.10. การค้นหา (Search): + +- ระบบต้องมีฟังก์ชันการค้นหาขั้นสูง ที่สามารถค้นหาเอกสาร **correspondence**, **rfa**, **shop_drawing**, **contract-drawing**, **transmittal** และ **ใบเวียน (Circulations)** จากหลายเงื่อนไขพร้อมกันได้ เช่น ค้นหาจากชื่อเรื่อง, ประเภท, วันที่, และ Tag + +## 4.11. การทำรายงาน (Reporting): + +- สามารถจัดทำรายงานสรุปแยกประเภทของ Correspondence ประจำวัน, สัปดาห์, เดือน, และปีได้ + +## 4.12. ประสิทธิภาพ (Performance): + +- มีการใช้ Caching กับข้อมูลที่เรียกใช้บ่อย และใช้ Pagination ในตารางข้อมูลเพื่อจัดการข้อมูลจำนวนมาก + +- ตัวชี้วัดประสิทธิภาพ: + + - **API Response Time:** < 200ms (90th percentile) สำหรับ operation ทั่วไป + - **Search Query Performance:** < 500ms สำหรับการค้นหาขั้นสูง + - **File Upload Performance:** < 30 seconds สำหรับไฟล์ขนาด 50MB + - **Concurrent Users:** รองรับผู้ใช้พร้อมกันอย่างน้อย 100 คน + - **Database Connection Pool:** ขนาดเหมาะสมกับ workload (default: min 5, max 20 connections) + - **Cache Hit Ratio:** > 80% สำหรับ cached data + - **Application Startup Time:** < 30 seconds + +- Caching Strategy: + + - **Master Data Cache:** Roles, Permissions, Organizations, Project metadata (TTL: 1 hour) + - **User Session Cache:** User permissions และ profile data (TTL: 30 minutes) + - **Search Result Cache:** Frequently searched queries (TTL: 15 minutes) + - **File Metadata Cache:** Attachment metadata (TTL: 1 hour) + - **Document Cache:** Frequently accessed document metadata (TTL: 30 minutes) + - **ต้องมี cache invalidation strategy ที่ชัดเจน:** + - Invalidate on update/delete operations + - Time-based expiration + - Manual cache clearance สำหรับ admin operations + - ใช้ Redis เป็น distributed cache backend + - ต้องมี cache monitoring (hit/miss ratios) + +- Frontend Performance: + - **Bundle Size Optimization:** ต้องควบคุมขนาด Bundle โดยรวมไม่เกิน 2MB + - **State Management Efficiency:** ใช้ State Management Libraries อย่างเหมาะสม ไม่เกิน 2 ตัวหลัก + - **Memory Management:** ต้องป้องกัน Memory Leak จาก State ที่ไม่จำเป็น + +## 4.13. System Security (ความปลอดภัยระบบ): + +- มีระบบ Rate Limiting เพื่อป้องกันการโจมตีแบบ Brute-force +- การจัดการ Secret (เช่น รหัสผ่าน DB, JWT Secret) จะต้องทำผ่าน Environment Variable ของ Docker เพื่อความปลอดภัยสูงสุด + - Rate Limiting Strategy: + - **Anonymous Endpoints:** 100 requests/hour ต่อ IP address + - **Authenticated Endpoints:** + - Viewer: 500 requests/hour + - Editor: 1000 requests/hour + - Document Control: 2000 requests/hour + - Admin/Superadmin: 5000 requests/hour + - **File Upload Endpoints:** 50 requests/hour ต่อ user + - **Search Endpoints:** 500 requests/hour ต่อ user + - **Authentication Endpoints:** 10 requests/minute ต่อ IP address + - **ต้องมี mechanism สำหรับยกเว้น rate limiting สำหรับ trusted services** + - ต้องบันทึก log เมื่อมีการ trigger rate limiting + - Error Handling และ Resilience: + - ต้องมี circuit breaker pattern สำหรับ external service calls + - ต้องมี retry mechanism ด้วย exponential backoff + - ต้องมี graceful degradation เมื่อบริการภายนอกล้มเหลว + - Error messages ต้องไม่เปิดเผยข้อมูล sensitive + - Input Validation: + - ต้องมี input validation ทั้งฝั่ง client และ server (defense in depth) + - ต้องป้องกัน OWASP Top 10 vulnerabilities: + - SQL Injection (ใช้ parameterized queries ผ่าน ORM) + - XSS (input sanitization และ output encoding) + - CSRF (CSRF tokens สำหรับ state-changing operations) + - ต้อง validate file uploads: + - File type (white-list approach) + - File size + - File content (magic number verification) + - ต้อง sanitize user inputs ก่อนแสดงผลใน UI + - ต้องใช้ content security policy (CSP) headers + - ต้องมี request size limits เพื่อป้องกัน DoS attacks + - Session และ Token Management: + - **JWT token expiration:** 8 hours สำหรับ access token + - **Refresh token expiration:** 7 days + - **Refresh token mechanism:** ต้องรองรับ token rotation และ revocation + - **Token revocation on logout:** ต้องบันทึก revoked tokens จนกว่าจะ expire + - **Concurrent session management:** + - จำกัดจำนวน session พร้อมกันได้ (default: 5 devices) + - ต้องแจ้งเตือนเมื่อมี login จาก device/location ใหม่ + - **Device fingerprinting:** สำหรับ security และ audit purposes + - **Password policy:** + - ความยาวขั้นต่ำ: 8 characters + - ต้องมี uppercase, lowercase, number, special character + - ต้องเปลี่ยน password ทุก 90 วัน + - ต้องป้องกันการใช้ password ที่เคยใช้มาแล้ว 5 ครั้งล่าสุด + +## 4.14. การสำรองข้อมูลและการกู้คืน (Backup & Recovery) + +- ระบบจะต้องมีกลไกการสำรองข้อมูลอัตโนมัติสำหรับฐานข้อมูล MariaDB [cite: 2.4] และไฟล์เอกสารทั้งหมดใน /share/dms-data [cite: 2.1] (เช่น ใช้ HBS 3 ของ QNAP หรือสคริปต์สำรองข้อมูล) อย่างน้อยวันละ 1 ครั้ง +- ต้องมีแผนการกู้คืนระบบ (Disaster Recovery Plan) ในกรณีที่ Server หลัก (QNAP) ใช้งานไม่ได้ +- ขั้นตอนการกู้คืน: + - **Database Restoration Procedure:** + - สร้างจาก full backup ล่าสุด + - Apply transaction logs ถึง point-in-time ที่ต้องการ + - Verify data integrity post-restoration + - **File Storage Restoration Procedure:** + - Restore จาก QNAP snapshot หรือ backup + - Verify file integrity และ permissions + - **Application Redeployment Procedure:** + - Deploy จาก version ล่าสุดที่รู้ว่าทำงานได้ + - Verify application health + - **Data Integrity Verification Post-Recovery:** + - Run data consistency checks + - Verify critical business data + - **Recovery Time Objective (RTO):** < 4 ชั่วโมง + - **Recovery Point Objective (RPO):** < 1 ชั่วโมง + +## 4.15. กลยุทธ์การแจ้งเตือน (Notification Strategy - ปรับปรุง) + +- ระบบจะส่งการแจ้งเตือน (ผ่าน Email หรือ Line [cite: 2.7]) เมื่อมีการกระทำที่สำคัญ** ดังนี้: + 1. เมื่อมีเอกสารใหม่ (Correspondence, RFA) ถูกส่งมาถึงองค์กรณ์ของเรา + 2. เมื่อมีใบเวียน (Circulation) ใหม่ มอบหมายงานมาที่เรา + 3. (ทางเลือก) เมื่อเอกสารที่เราส่งไป ถูกดำเนินการ (เช่น อนุมัติ/ปฏิเสธ) + 4. (ทางเลือก) เมื่อใกล้ถึงวันครบกำหนด (Deadline) [cite: 3.2.5, 3.6.6, 3.7.5] +- Grouping/Digest + - กรณีมีการแจ้งเตือนประเภทเดียวกันจำนวนมากในช่วงเวลาสั้นๆ (เช่น Approve เอกสาร 10 ฉบับรวด) ระบบต้อง **รวม (Batch)** เป็น 1 Email/Line Notification เพื่อไม่ให้รบกวนผู้ใช้ (Spamming) +- Notification Delivery Guarantees + - **At-least-once delivery:** สำหรับ important notifications + - **Retry mechanism:** ด้วย exponential backoff (max 3 reties) + - **Dead letter queue:** สำหรับ notifications ที่ส่งไม่สำเร็จหลังจาก retries + - **Delivery status tracking:** ต้องบันทึกสถานะการส่ง notifications + - **Fallback channels:** ถ้า Email ล้มเหลว ให้ส่งผ่าน SYSTEM notification + - **Notification preferences:** ผู้ใช้ต้องสามารถกำหนด channel preferences ได้ + +## 4.16. Maintenance Mode + +- ระบบต้องมีกลไก **Maintenance Mode** ที่ Admin สามารถเปิดใช้งานได้ + - เมื่อเปิด: ผู้ใช้ทั่วไปจะเห็นหน้า "ปิดปรับปรุง" และไม่สามารถเรียก API ได้ (ยกเว้น Admin) + - ใช้สำหรับช่วง Deploy Version ใหม่ หรือ Database Migration + +## 4.17. Monitoring และ Observability + +- Application Monitoring + - **Health checks:** /health endpoint สำหรับ load balancer + - **Metrics collection:** Response times, error rates, throughput + - **Distributed tracing:** สำหรับ request tracing across services + - **Log aggregation:** Structured logging ด้วย JSON format + - **Alerting:** สำหรับ critical errors และ performance degradation +- Business Metrics + - จำนวน documents created ต่อวัน + - Workflow completion rates + - User activity metrics + - System utilization rates + - Search query performance +- Security Monitoring + - Failed login attempts + - Rate limiting triggers + - Virus scan results + - File download activities + - Permission changes + +## 4.18. JSON Processing & Validation + +- JSON Schema Management + - ต้องมี centralized JSON schema registry + - ต้องรองรับ schema versioning และ migration + - ต้องมี schema validation during runtime +- Performance Optimization + - **Caching:** Cache parsed JSON structures + - **Compression:** ใช้ compression สำหรับ JSON ขนาดใหญ่ + - **Indexing:** Support JSON path indexing สำหรับ query +- Error Handling + - ต้องมี graceful degradation เมื่อ JSON validation ล้มเหลว + - ต้องมี default fallback values + - ต้องบันทึก error logs สำหรับ validation failures + + +# 5. UI/UX Guidelines +# 👥 Section 5: UI/UX Requirements (ข้อกำหนดด้านผู้ใช้งาน) +--- +title: 'UI/UX Requirements' +version: 1.5.0 +status: first-draft +owner: Nattanin Peancharoen +last_updated: 2025-11-30 +related: + +- specs/02-architecture/data-model.md#correspondence +- specs/03-implementation/backend-guidelines.md#correspondencemodule +--- + +## 5.1. Layout หลัก + +- หน้าเว็บใช้รูปแบบ App Shell ที่ประกอบด้วย + - Navbar (ส่วนบน): แสดงชื่อระบบ, เมนูผู้ใช้ (Profile), เมนูสำหรับ Document Control/เมนูสำหรับ Admin/Superadmin (จัดการผู้ใช้, จัดการสิทธิ์, และอื่นๆ), และปุ่ม Login/Logout + - Sidebar (ด้านข้าง): เป็นเมนูหลักสำหรับเข้าถึงส่วนที่เกี่ยวข้องกับเอกสารทั้งหมด เช่น Dashboard, Correspondences, RFA, Drawings + - Main Content Area: พื้นที่สำหรับแสดงเนื้อหาหลักของหน้าที่เลือก + +## 5.2. หน้า Landing Page + +- เป็นหน้าแรกที่แสดงข้อมูลบางส่วนของโครงการสำหรับผู้ใช้ที่ยังไม่ได้ล็อกอิน + +## 5.3. หน้า Dashboard + +- เป็นหน้าแรกหลังจากล็อกอิน ประกอบด้วย + - การ์ดสรุปภาพรวม (KPI Cards): แสดงข้อมูลสรุปที่สำคัญขององค์กร เช่น จำนวนเอกสาร, งานที่เกินกำหนด + - ตาราง "งานของฉัน" (My Tasks Table): แสดงรายการงานทั้งหมดจาก Circulation ที่ผู้ใช้ต้องดำเนินการ + - Security Metrics: แสดงจำนวน files scanned, security incidents, failed login attempts + +## 5.4. การติดตามสถานะ + +- องค์กรสามารถติดตามสถานะเอกสารทั้งของตนเอง (Originator) และสถานะเอกสารที่ส่งมาถึงตนเอง (Recipient) + +## 5.5. การจัดการข้อมูลส่วนตัว (Profile Page) + +- ผู้ใช้สามารถจัดการข้อมูลส่วนตัวและเปลี่ยนรหัสผ่านของตนเองได้ + +## 5.6. การจัดการเอกสารทางเทคนิค (RFA) + +- ผู้ใช้สามารถดู RFA ในรูปแบบ Workflow Diagram ทั้งหมดได้ในหน้าเดียว +- Interactive History (เพิ่ม): ในแผนภาพ Workflow ผู้ใช้ต้องสามารถ คลิกที่ Node หรือ Step เก่าที่ผ่านมาแล้ว เพื่อดู Audit Log ย่อยของ Step นั้นได้ทันที (เช่น ใครเป็นคนกด Approve, เวลาไหน, มี Comment อะไร) โดยไม่ต้องสลับไปดูใน Tab History แยกต่างหาก +- ขั้นตอนที่ยังไม่ถึงหรือผ่านไปแล้วจะเป็นรูปแบบ disabled +- สามารถดำเนินการได้เฉพาะในขั้นตอนที่ได้รับมอบหมายงาน (active) +- สิทธิ์ Document Control ขึ้นไป สามารถกด "Force Proceed" ไปยังขั้นตอนต่อไปได้ทุกขั้นตอน, หรือ "Revert" กลับขั้นตอนก่อนหน้าได้ + +## 5.7. การจัดการใบเวียนเอกสาร (Circulation) + +- ผู้ใช้สามารถดู Circulation ในรูปแบบ Workflow ทั้งหมดได้ในหน้าเดียว,ขั้นตอนที่ยังไม่ถึงหรือผ่านไปแล้วจะเป็นรูปแบบ disabled, สามารถดำเนินการได้เฉพาะในขั้นตอนที่ได้รับมอบหมายงาน (active) เช่น ตรวจสอบแล้ว เพื่อไปยังขั้นตอนต่อไป, สิทธิ์ Document Control ขึ้นไป สามารถกด ไปยังขั้นตอนต่อไป ได้ทุกขั้นตอน, การย้อนกลับ ไปขั้นตอนก่อนหน้า สามารถทำได้โดย สิทธิ์ Document Control ขึ้นไป + +## 5.8. การจัดการเอกสารนำส่ง (Transmittals) + +- ผู้ใช้สามารถดู Transmittals ในรูปแบบรายการทั้งหมดได้ในหน้าเดียว + +## 5.9. ข้อกำหนด UI/UX การแนบไฟล์ (File Attachment UX) + +- ระบบต้องรองรับการอัปโหลดไฟล์หลายไฟล์พร้อมกัน (Multi-file upload) เช่น การลากและวาง (Drag-and-Drop) +- ในหน้าอัปโหลด (เช่น สร้าง RFA หรือ Correspondence) ผู้ใช้ต้องสามารถกำหนดได้ว่าไฟล์ใดเป็น "เอกสารหลัก" (Main Document เช่น PDF) และไฟล์ใดเป็น "เอกสารแนบประกอบ" (Supporting Attachments เช่น .dwg, .docx, .zip) +- **Security Feedback:** แสดง security warnings สำหรับ file types ที่เสี่ยงหรือ files ที่ fail virus scan +- **File Type Indicators:** แสดง file type icons และ security status + +## 5.10 Form & Interaction + +- **Dynamic Form Generator:** ใช้ Component กลางที่รับ JSON Schema แล้ว Render Form ออกมาอัตโนมัติ เพื่อลดความซ้ำซ้อนของโค้ดหน้าบ้าน และรองรับเอกสารประเภทใหม่ๆ ได้ทันที +- **Optimistic Updates:** การเปลี่ยนสถานะ (เช่น กด Approve, กด Read) ให้ UI เปลี่ยนสถานะทันทีให้ผู้ใช้เห็นก่อนรอ API Response (Rollback ถ้า Failed) + +## 5.11 Mobile Responsiveness + +- **Table Visualization:** บนหน้าจอมือถือ ตารางข้อมูลที่มีหลาย Column (เช่น Correspondence List) ต้องเปลี่ยนการแสดงผลเป็นแบบ **Card View** อัตโนมัติ +- **Navigation:** Sidebar ต้องเป็นแบบ Collapsible Drawer + +## 5.12 Resilience & Offline Support + +- **Auto-Save Draft:** ระบบต้องบันทึกข้อมูลฟอร์มที่กำลังกรอกลง **LocalStorage** อัตโนมัติ เพื่อป้องกันข้อมูลหายกรณีเน็ตหลุดหรือปิด Browser โดยไม่ได้ตั้งใจ +- **State Management:** ใช้ State Management ที่เหมาะสมและไม่ซับซ้อนเกินไป โดยเน้นการใช้ React Query สำหรับ Server State และ React Hook Form สำหรับ Form State +- **Graceful Degradation:** หาก Service รอง (เช่น Search, Notification) ล่ม ระบบหลัก (CRUD) ต้องยังทำงานต่อได้ + +## 5.13. Secure In-App PDF Viewer (ใหม่) + +- 5.13.1 Viewer Capabilities: ระบบต้องมี PDF Viewer ภายในแอปพลิเคชันที่สามารถเปิดดูไฟล์เอกสารหลัก (PDF) ได้ทันทีโดยไม่ต้องดาวน์โหลดลงเครื่อง เพื่อความสะดวกในการตรวจทาน (Review/Approve) +- 5.13.2 Security: การแสดงผลไฟล์ต้อง ห้าม (Disable) การทำ Browser Cache สำหรับไฟล์ Sensitive เพื่อป้องกันการกู้คืนไฟล์จากเครื่อง Client ภายหลัง +- 5.13.3 Performance: ต้องรองรับการส่งข้อมูลแบบ Streaming (Range Requests) เพื่อให้เปิดดูไฟล์ขนาดใหญ่ (เช่น แบบแปลน 50MB+) ได้รวดเร็วโดยไม่ต้องรอโหลดเสร็จทั้งไฟล์ + +## 🧪 6. Testing Requirements +## 6.1 Unit Testing + +- ต้องมี unit tests สำหรับ business logic ทั้งหมด +- Code coverage อย่างน้อย 70% สำหรับ backend services + - Business Logic: 80%+ + - Controllers: 70%+ + - Utilities: 90%+ +- ต้องทดสอบ RBAC permission logic ทุกระดับ + +## 6.2 Integration Testing + +- ทดสอบการทำงานร่วมกันของ modules +- ทดสอบ database migrations และ data integrity +- ทดสอบ API endpoints ด้วย realistic data + +## 6.3 End-to-End Testing + +- ทดสอบ complete user workflows +- ทดสอบ document lifecycle จาก creation ถึง archival +- ทดสอบ cross-module integrations + +## 6.4 Security Testing + +- Penetration Testing: ทดสอบ OWASP Top 10 vulnerabilities +- Security Audit: Review code สำหรับ security flaws +- Virus Scanning Test: ทดสอบ file upload security +- Rate Limiting Test: ทดสอบ rate limiting functionality + +## 6.5 Performance Testing + +- **Load Testing:** ทดสอบด้วย realistic workloads +- **Stress Testing:** หา breaking points ของระบบ +- **Endurance Testing:** ทดสอบการทำงานต่อเนื่องเป็นเวลานาน + +## 6.6 Disaster Recovery Testing + +- ทดสอบ backup และ restoration procedures +- ทดสอบ failover mechanisms +- ทดสอบ data integrity หลังการ recovery + +## 6.7 Specific Scenario Testing (เพิ่ม) + +- **Race Condition Test:** ทดสอบยิง Request ขอเลขที่เอกสารพร้อมกัน 100 Request + - **Transaction Test:** ทดสอบปิดเน็ตระหว่าง Upload ไฟล์ (ตรวจสอบว่าไม่มี Orphan File หรือ Broken Link) + - **Permission Test:** ทดสอบ CASL Integration ทั้งฝั่ง Backend และ Frontend ให้ตรงกัน + +--- +*Version History* +- **v1.5.1** – 2025‑12‑04 – Consolidated requirement specifications into single document. diff --git a/docs/1_FullStackJS_V1_5_1.md b/docs/1_FullStackJS_V1_5_1.md new file mode 100644 index 0000000..0d00d2d --- /dev/null +++ b/docs/1_FullStackJS_V1_5_1.md @@ -0,0 +1,1448 @@ +# 📝 **Documents Management System Version 1.5.1: แนวทางการพัฒนา FullStackJS** + +**สถานะ:** FINAL GUIDELINE Rev.06 +**วันที่:** 2025-12-04 +**อ้างอิง:** Requirements Specification v1.5.1 +**Classification:** Internal Technical Documentation + +## 🧠 **1. ปรัชญาทั่วไป (General Philosophy)** + +แนวทางปฏิบัติที่ดีที่สุดแบบครบวงจรสำหรับการพัฒนา NestJS Backend, NextJS Frontend และ Tailwind-based UI/UX ในสภาพแวดล้อม TypeScript มุ่งเน้นที่ **"Data Integrity First"** (ความถูกต้องของข้อมูลต้องมาก่อน) ตามด้วย Security และ UX + +- **ความชัดเจน (clarity), ความง่ายในการบำรุงรักษา (maintainability), ความสอดคล้องกัน (consistency) และ การเข้าถึงได้ (accessibility)** ตลอดทั้งสแต็ก +- **Strict Typing:** ใช้ TypeScript อย่างเคร่งครัด ห้าม `any` +- **Consistency:** ใช้ภาษาอังกฤษใน Code / ภาษาไทยใน Comment +- **Resilience:** ระบบต้องทนทานต่อ Network Failure และ Race Condition + +## ⚙️ **2. แนวทางทั่วไปสำหรับ TypeScript** + +### **2.1 หลักการพื้นฐาน** + +- ใช้ **ภาษาอังกฤษ** สำหรับโค้ด +- ใช้ **ภาษาไทย** สำหรับ comment และเอกสารทั้งหมด +- กำหนดไทป์ (type) อย่างชัดเจนสำหรับตัวแปร, พารามิเตอร์ และค่าที่ส่งกลับ (return values) ทั้งหมด +- หลีกเลี่ยงการใช้ any; ให้สร้างไทป์ (types) หรืออินเทอร์เฟซ (interfaces) ที่กำหนดเอง +- ใช้ **JSDoc** สำหรับคลาส (classes) และเมธอด (methods) ที่เป็น public +- ส่งออก (Export) **สัญลักษณ์หลัก (main symbol) เพียงหนึ่งเดียว** ต่อไฟล์ +- หลีกเลี่ยงบรรทัดว่างภายในฟังก์ชัน +- ระบุ // File: path/filename ในบรรทัดแรกของทุกไฟล์ +- ระบุ // บันทึกการแก้ไข, หากมีการแก้ไขเพิ่มในอนาคต ให้เพิ่มบันทึก + +### **2.2 Configuration & Secrets Management** + +- **Production/Staging:** + - ใช้ Docker secrets หรือ environment variables ที่ inject ผ่าน CI/CD + - พิจารณา Hashicorp Vault หรือ AWS Secrets Manager สำหรับ production + - ห้ามใส่ Secrets (Password, Keys) ใน `docker-compose.yml` หลัก +- **Development:** + - ใช้ `docker-compose.override.yml` (gitignored) สำหรับ local secrets + - ไฟล์ `docker-compose.yml` หลักใช้ค่า dummy/placeholder +- **Validation:** + - ใช้ `joi` หรือ `zod` ในการ Validate Environment Variables ตอน Start App หากขาดตัวแปรสำคัญให้ Throw Error ทันที + +### **2.3 Idempotency (ความสามารถในการทำซ้ำได้)** + +- สำหรับการทำงานที่สำคัญ (Create Document, Approve, Transactional) **ต้อง** ออกแบบให้เป็น Idempotent +- Client **ต้อง** ส่ง Header `Idempotency-Key` (UUID) มากับ Request +- Server **ต้อง** ตรวจสอบว่า Key นี้เคยถูกประมวลผลสำเร็จไปแล้วหรือไม่ ถ้าใช่ ให้คืนค่าเดิมโดยไม่ทำซ้ำ + +### **2.4 ข้อตกลงในการตั้งชื่อ (Naming Conventions)** + +| Entity (สิ่งที่ตั้งชื่อ) | Convention (รูปแบบ) | Example (ตัวอย่าง) | +| :-------------------- | :----------------- | :--------------------------------- | +| Classes | PascalCase | UserService | +| Property | snake_case | user_id | +| Variables & Functions | camelCase | getUserInfo | +| Files & Folders | kebab-case | user-service.ts | +| Environment Variables | UPPERCASE | DATABASE_URL | +| Booleans | Verb + Noun | isActive, canDelete, hasPermission | + +ใช้คำเต็ม — ไม่ใช้อักษรย่อ — ยกเว้นคำมาตรฐาน (เช่น API, URL, req, res, err, ctx) + +### 🧩**2.5 ฟังก์ชัน (Functions)** + +- เขียนฟังก์ชันให้สั้น และทำ **หน้าที่เพียงอย่างเดียว** (single-purpose) (\< 20 บรรทัด) +- ใช้ **early returns** เพื่อลดการซ้อน (nesting) ของโค้ด +- ใช้ **map**, **filter**, **reduce** แทนการใช้ loops เมื่อเหมาะสม +- ควรใช้ **arrow functions** สำหรับตรรกะสั้นๆ, และใช้ **named functions** ในกรณีอื่น +- ใช้ **default parameters** แทนการตรวจสอบค่า null +- จัดกลุ่มพารามิเตอร์หลายตัวให้เป็นอ็อบเจกต์เดียว (RO-RO pattern) +- ส่งค่ากลับ (Return) เป็นอ็อบเจกต์ที่มีไทป์กำหนด (typed objects) ไม่ใช่ค่าพื้นฐาน (primitives) +- รักษาระดับของสิ่งที่เป็นนามธรรม (abstraction level) ให้เป็นระดับเดียวในแต่ละฟังก์ชัน + +### 🧱**2.6 การจัดการข้อมูล (Data Handling)** + +- ห่อหุ้มข้อมูล (Encapsulate) ในไทป์แบบผสม (composite types) +- ใช้ **immutability** (การไม่เปลี่ยนแปลงค่า) ด้วย readonly และ as const +- ทำการตรวจสอบความถูกต้องของข้อมูล (Validations) ในคลาสหรือ DTOs ไม่ใช่ภายในฟังก์ชันทางธุรกิจ +- ตรวจสอบความถูกต้องของข้อมูลโดยใช้ DTOs ที่มีไทป์กำหนดเสมอ + +### 🧰**2.7 คลาส (Classes)** + +- ปฏิบัติตามหลักการ **SOLID** +- ควรใช้ **composition มากกว่า inheritance** (Prefer composition over inheritance) +- กำหนด **interfaces** สำหรับสัญญา (contracts) +- ให้คลาสมุ่งเน้นการทำงานเฉพาะอย่างและมีขนาดเล็ก (\< 200 บรรทัด, \< 10 เมธอด, \< 10 properties) + +### 🚨**2.8 การจัดการข้อผิดพลาด (Error Handling)** + +- ใช้ Exceptions สำหรับข้อผิดพลาดที่ไม่คาดคิด +- ดักจับ (Catch) ข้อผิดพลาดเพื่อแก้ไขหรือเพิ่มบริบท (context) เท่านั้น; หากไม่เช่นนั้น ให้ใช้ global error handlers +- ระบุข้อความข้อผิดพลาด (error messages) ที่มีความหมายเสมอ + +### 🧪**2.9 การทดสอบ (ทั่วไป) (Testing (General))** + +- ใช้รูปแบบ **Arrange–Act–Assert** +- ใช้ชื่อตัวแปรในการทดสอบที่สื่อความหมาย (inputData, expectedOutput) +- เขียน **unit tests** สำหรับ public methods ทั้งหมด +- จำลอง (Mock) การพึ่งพาภายนอก (external dependencies) +- เพิ่ม **acceptance tests** ต่อโมดูลโดยใช้รูปแบบ Given–When-Then + +### **Testing Strategy โดยละเอียด** + +- **Test Pyramid Structure** + + /\ + + / \ E2E Tests (10%) + /\_**\_\ Integration Tests (20%) + / \ Unit Tests (70%) + /**\_\_\*\*\*\*\ + +- **Testing Tools Stack** + +```typescript +// Backend Testing Stack +const backendTesting = { + unit: ['Jest', 'ts-jest', '@nestjs/testing'], + integration: ['Supertest', 'Testcontainers', 'Jest'], + e2e: ['Supertest', 'Jest', 'Database Seeds'], + security: ['Jest', 'Custom Security Test Helpers'], + performance: ['Jest', 'autocannon', 'artillery'], +}; + +// Frontend Testing Stack +const frontendTesting = { + unit: ['Vitest', 'React Testing Library'], + integration: ['React Testing Library', 'MSW'], + e2e: ['Playwright', 'Jest'], + visual: ['Playwright', 'Loki'], +}; +``` + +- **Test Data Management** + +```typescript +// Test Data Factories +interface TestDataFactory { + createUser(overrides?: Partial): User; + createCorrespondence(overrides?: Partial): Correspondence; + createRoutingTemplate(overrides?: Partial): RoutingTemplate; +} + +// Test Scenarios +const testScenarios = { + happyPath: 'Normal workflow execution', + edgeCases: 'Boundary conditions and limits', + errorConditions: 'Error handling and recovery', + security: 'Authentication and authorization', + performance: 'Load and stress conditions', +}; +``` + +## 🏗️ **3. แบ็กเอนด์ (NestJS) - Implementation Details** + +### **3.1 หลักการ** + +- **สถาปัตยกรรมแบบโมดูลาร์ (Modular architecture)**: + - หนึ่งโมดูลต่อหนึ่งโดเมน + - โครงสร้างแบบ Controller → Service → Repository (Model) +- API-First: มุ่งเน้นการสร้าง API ที่มีคุณภาพสูง มีเอกสารประกอบ (Swagger) ที่ชัดเจนสำหรับ Frontend Team +- DTOs ที่ตรวจสอบความถูกต้องด้วย **class-validator** +- ใช้ **MikroORM** (หรือ TypeORM/Prisma) สำหรับการคงอยู่ของข้อมูล (persistence) ซึ่งสอดคล้องกับสคีมา MariaDB +- ห่อหุ้มโค้ดที่ใช้ซ้ำได้ไว้ใน **common module** (@app/common): + - Configs, decorators, DTOs, guards, interceptors, notifications, shared services, types, validators + +### **3.2 Database & Data Modeling (MariaDB + TypeORM)** + +#### **3.2.1 Optimistic Locking & Versioning** + +เพื่อป้องกัน Race Condition ในการแก้ไขข้อมูลพร้อมกัน (โดยเฉพาะการรันเลขที่เอกสาร) ให้เพิ่ม Column `@VersionColumn()` ใน Entity ที่สำคัญ + +```typescript +@Entity() +export class DocumentNumberCounter { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + last_number: number; + + @VersionColumn() // Auto-increment on update + version: number; +} +``` + +#### **3.2.2 Virtual Columns for JSON Performance** + +เนื่องจากเราใช้ MariaDB 10.11 และมีการเก็บข้อมูล JSON (Details) ให้ใช้ **Generated Columns (Virtual)** สำหรับ Field ที่ต้อง Search/Sort บ่อยๆ และทำ Index บน Virtual Column นั้น + +```sql +-- ตัวอย่าง SQL Migration +ALTER TABLE correspondence_revisions +ADD COLUMN ref_project_id INT GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(details, '$.projectId'))) VIRTUAL; +CREATE INDEX idx_ref_project_id ON correspondence_revisions(ref_project_id); +``` + +#### **3.2.3 Partitioning Strategy** + +- สำหรับตาราง `audit_logs` และ `notifications` ให้เตรียมออกแบบ Entity ให้รองรับ Partitioning (เช่น แยกตามปี) โดยใช้ Raw SQL Migration ในการสร้างตาราง +- Automated Partition Maintenance: ต้องมี Cron Job (Scheduled Task) เพื่อตรวจสอบและสร้าง Partition สำหรับปี/เดือนถัดไปล่วงหน้า (Pre-create partitions) อย่างน้อย 1 เดือน เพื่อป้องกัน Insert Error เมื่อขึ้นช่วงเวลาใหม่ + +### **3.3 File Storage Service (Two-Phase Storage)** + +ปรับปรุง Service จัดการไฟล์ให้รองรับ Transactional Integrity + +**Phase 1: Upload to Temp** + +```typescript +@Post('upload') +async uploadFile(@UploadedFile() file: Express.Multer.File) { + // 1. Virus Scan + await this.virusScan(file); + + // 2. Save to temp/ + const tempId = await this.fileStorage.saveToTemp(file); + + // 3. Return temp_id + return { temp_id: tempId, expires_at: addHours(new Date(), 24) }; +} +``` + +**Phase 2: Commit to Permanent** + +```typescript +async createCorrespondence(dto: CreateDto, tempFileIds: string[]) { + return this.dataSource.transaction(async (manager) => { + // 1. Create Correspondence + const correspondence = await manager.save(Correspondence, dto); + + // 2. Commit Files (ภายใน Transaction) + await this.fileStorage.commitFiles(tempFileIds, correspondence.id, manager); + + return correspondence; + }); +} +``` + +**Cleanup Job:** + +```typescript +@Cron('0 */6 * * *') // ทุก 6 ชั่วโมง +async cleanupOrphanFiles() { + const expiredFiles = await this.attachmentRepo.find({ + where: { + is_temporary: true, + expires_at: LessThan(new Date()), + }, + }); + + for (const file of expiredFiles) { + await this.deleteFile(file.file_path); + await this.attachmentRepo.remove(file); + } +} +``` + +### **3.4 Document Numbering (Double-Lock Mechanism)** + +การออกเลขที่เอกสารต้องใช้กลไกความปลอดภัย 2 ชั้น: + +**Double-Lock Mechanism Implementation:** + +```typescript +@Injectable() +export class DocumentNumberingService { + async generateNextNumber(context: NumberingContext): Promise { + const lockKey = `doc_num:${context.projectId}:${context.typeId}`; + + // Layer 1: Redis Lock (2-5 seconds TTL) + const lock = await this.redisLock.acquire(lockKey, 3000); + + try { + // Layer 2: Optimistic DB Lock + const counter = await this.counterRepo.findOne({ + where: context, + lock: { mode: 'optimistic' }, + }); + + counter.last_number++; + await this.counterRepo.save(counter); // Throws if version changed + + return this.formatNumber(counter); + } finally { + await lock.release(); + } + } +} +``` + +### **3.5 Unified Workflow Engine** + +Unified Workflow Engine (Core Architecture) + +- ระบบใช้ Workflow Engine เป็นหัวใจหลักในการขับเคลื่อน State ของเอกสาร: + - DSL Based: Logic ทั้งหมดอยู่ที่ workflow_definitions.dsl + - Instance Based: สถานะปัจจุบันอยู่ที่ workflow_instances + - Module Integration: + - CorrespondenceModule -> เรียก WorkflowEngine + - RfaModule -> เรียก WorkflowEngine + - CirculationModule -> เรียก WorkflowEngine +- ห้าม สร้างตาราง Routing แยก (เช่น rfa_workflows หรือ correspondence_routings) อีกต่อไป +- Boot-time Validation: + - เมื่อ Application Start (Backend Boot), ระบบต้องทำการ Validate Workflow DSL Definitions ทั้งหมด ว่า Syntax ถูกต้องและ State Transitions เชื่อมโยงกันสมบูรณ์ หากพบข้อผิดพลาดให้ Alert หรือ Block Startup (ใน Development Mode) เพื่อป้องกัน Runtime Error + +### **3.6 ฟังก์ชันหลัก (Core Functionalities)** + +- Global **filters** สำหรับการจัดการ exception +- **Middlewares** สำหรับการจัดการ request +- **Guards** สำหรับการอนุญาต (permissions) และ RBAC +- **Interceptors** สำหรับการแปลงข้อมูล response และการบันทึก log + +### **3.6.1 Idempotency Interceptor** + +```typescript +@Injectable() +export class IdempotencyInterceptor implements NestInterceptor { + async intercept(context: ExecutionContext, next: CallHandler) { + const request = context.switchToHttp().getRequest(); + const idempotencyKey = request.headers['idempotency-key']; + + if (!idempotencyKey) { + throw new BadRequestException('Idempotency-Key required'); + } + + // ตรวจสอบ Cache + const cached = await this.redis.get(`idempotency:${idempotencyKey}`); + if (cached) { + return of(JSON.parse(cached)); // Return ผลลัพธ์เดิม + } + + // Execute & Cache Result + return next.handle().pipe( + tap(async (response) => { + await this.redis.set( + `idempotency:${idempotencyKey}`, + JSON.stringify(response), + 'EX', + 86400 // 24 hours + ); + }) + ); + } +} +``` + +### **3.7 ข้อจำกัดในการ Deploy (QNAP Container Station)** + +- **ห้ามใช้ไฟล์ .env** ในการตั้งค่า Environment Variables [cite: 2.1] + +### **3.8 ข้อจำกัดด้านความปลอดภัย (Security Constraints):** + +- **File Upload Security:** ต้องมี virus scanning (ClamAV), file type validation (white-list), และ file size limits (50MB) +- **Input Validation:** ต้องป้องกัน OWASP Top 10 vulnerabilities (SQL Injection, XSS, CSRF) +- **Rate Limiting:** ต้อง implement rate limiting ตาม strategy ที่กำหนด +- **Secrets Management:** ต้องมี mechanism สำหรับจัดการ sensitive secrets อย่างปลอดภัย แม้จะใช้ docker-compose.yml + +### **3.9 โครงสร้างโมดูลตามโดเมน (Domain-Driven Module Structure)** + +เพื่อให้สอดคล้องกับสคีมา SQL (LCBP3-DMS) เราจะใช้โครงสร้างโมดูลแบบ **Domain-Driven (แบ่งตามขอบเขตธุรกิจ)** แทนการแบ่งตามฟังก์ชัน: + +#### 3.9.1 **CommonModule:** + +- เก็บ Services ที่ใช้ร่วมกัน เช่น DatabaseModule, FileStorageService (จัดการไฟล์ใน QNAP), AuditLogService, NotificationService +- จัดการ audit_logs +- NotificationService ต้องรองรับ Triggers ที่ระบุใน Requirement 6.7 [cite: 6.7] + +#### 3.9.2 **AuthModule:** + +- จัดการะการยืนยันตัวตน (JWT, Guards) +- **(สำคัญ)** ต้องรับผิดชอบการตรวจสอบสิทธิ์ **4 ระดับ** [cite: 4.2]: สิทธิ์ระดับระบบ (Global Role), สิทธิ์ระดับองกรณ์ (Organization Role), สิทธิ์ระดับโปรเจกต์ (Project Role), และ สิทธิ์ระดับสัญญา (Contract Role) +- **(สำคัญ)** ต้องมี API สำหรับ **Admin Panel** เพื่อ: + - สร้างและจัดการ Role และการจับคู่ Permission แบบไดนามิก [cite: 4.3] + - ให้ Superadmin สร้าง Organizations และกำหนด Org Admin ได้ [cite: 4.6] + - ให้ Superadmin/Admin จัดการ document_number_formats (รูปแบบเลขที่เอกสาร), document_number_counters (Running Number) [cite: 3.10] + +#### 3.9.3 **UserModule:** + +- จัดการ users, roles, permissions, global_default_roles, role_permissions, user_roles, user_project_roles +- **(สำคัญ)** ต้องมี API สำหรับ **Admin Panel** เพื่อ: + - สร้างและจัดการ Role และการจับคู่ Permission แบบไดนามิก [cite: 4.3] + +#### 3.9.4 **ProjectModule:** + +- จัดการ projects, organizations, contracts, project_parties, contract_parties + +#### 3.9.5 **MasterModule:** + +- จัดการ master data (correspondence_types, rfa_types, rfa_status_codes, rfa_approve_codes, circulation_status_codes, correspondence_types, correspondence_status, tags) [cite: 4.5] + +#### 3.9.6 **CorrespondenceModule (โมดูลศูนย์กลาง):** + +- จัดการ correspondences, correspondence_revisions, correspondence_tags +- **(สำคัญ)** Service นี้ต้อง Inject DocumentNumberingService เพื่อขอเลขที่เอกสารใหม่ก่อนการสร้าง +- **(สำคัญ)** ตรรกะการสร้าง/อัปเดต Revision จะอยู่ใน Service นี้ +- จัดการ correspondence_attachments (ตารางเชื่อมไฟล์แนบ) +- รับผิดชอบ Routing **Correspondence WorkflowService** เป็น Adapter เชื่อมต่อกับ Engine สำหรับการส่งต่อเอกสารทั่วไประหว่างองค์กร + +#### 3.9.7 **RfaModule:** + +- จัดการ rfas, rfa_revisions, rfa_items +- รับผิดชอบเวิร์กโฟลว์ **"RFA WorkflowService"** เป็น Adapter เชื่อมต่อกับ Engine สำหรับการอนุมัติเอกสารทางเทคนิค + +#### 3.9.8 **DrawingModule:** + +- จัดการ shop_drawings, shop_drawing_revisions, contract_drawings, contract_drawing_volumes, contract_drawing_cats, contract_drawing_sub_cats, shop_drawing_main_categories, shop_drawing_sub_categories, contract_drawing_subcat_cat_maps, shop_drawing_revision_contract_refs +- จัดการ shop_drawing_revision_attachments และ contract_drawing_attachments(ตารางเชื่อมไฟล์แนบ) + +#### 3.9.9 **CirculationModule:** + +- จัดการ circulations, circulation_templates, circulation_assignees +- จัดการ circulation_attachments (ตารางเชื่อมไฟล์แนบ) +- รับผิดชอบเวิร์กโฟลว์ **"Circulations WorkflowService"** เป็น Adapter เชื่อมต่อกับ Engine สำหรับการเวียนเอกสาร **ภายในองค์กร** + +#### 3.9.10 **TransmittalModule:** + +- จัดการ transmittals และ transmittal_items + +#### 3.9.11 **SearchModule:** + +- ให้บริการค้นหาขั้นสูง (Advanced Search) [cite: 6.2] โดยใช้ **Elasticsearch** เพื่อรองรับการค้นหาแบบ Full-text จากชื่อเรื่อง, รายละเอียด, เลขที่เอกสาร, ประเภท, วันที่, และ Tags +- ระบบจะใช้ Elasticsearch Engine ในการจัดทำดัชนีเพื่อการค้นหาข้อมูลเชิงลึกจากเนื้อหาของเอกสาร โดยข้อมูลจะถูกส่งไปทำดัชนีจาก Backend (NestJS) ทุกครั้งที่มีการสร้างหรือแก้ไขเอกสาร + +#### 3.9.12 **DocumentNumberingModule:** + +- **สถานะ:** เป็น Module ภายใน (Internal Module) ไม่เปิด API สู่ภายนอก +- **หน้าที่:** ให้บริการ `DocumentNumberingService` แบบ **Token-Based Generator** +- **Logic ใหม่ (v1.4.4):** + - รับ Context: `{ projectId, orgId, typeId, disciplineId?, subTypeId?, year }` + - ดึง Template จาก DB + - Parse Template เพื่อหาว่าต้องใช้ Key ใดบ้างในการทำ Grouping Counter (เช่น ถ้า Template มี `{DISCIPLINE}` ให้ใช้ `discipline_id` ในการ query counter) + - ใช้ **Double-Lock Mechanism** (Redis + Optimistic DB Lock) ในการดึงและอัพเดทค่า `last_number` + - Lock Timeout: การ Acquire Redis Lock ต้องกำหนด TTL (Time-to-Live) ที่สั้นและเหมาะสม (เช่น 2-5 วินาที) เพื่อป้องกัน Deadlock กรณี Service Crash ระหว่างทำงาน + - Retry Logic: ต้องมี Retry mechanism แบบ Exponential Backoff (แนะนำ 3-5 ครั้ง) หากไม่สามารถ Acquire Lock ได้ +- **Features:** + - Application-level locking เพื่อป้องกัน race condition + - Retry mechanism ด้วย exponential backoff + - Fallback mechanism เมื่อการขอเลขล้มเหลว + - Audit log ทุกครั้งที่มีการ generate เลขที่เอกสารใหม่ + +#### 3.9.13 **CorrespondenceRoutingModule:** + +- **สถานะ:** โมดูลหลักสำหรับจัดการการส่งต่อเอกสาร +- **หน้าที่:** จัดการแม่แบบการส่งต่อและการส่งต่อจริง +- **Entities:** + - CorrespondenceRoutingTemplate + - CorrespondenceRoutingTemplateStep + - CorrespondenceRouting +- **Features:** + - สร้างและจัดการแม่แบบการส่งต่อ + - ดำเนินการส่งต่อเอกสารตามแม่แบบ + - ติดตามสถานะการส่งต่อ + - คำนวณวันครบกำหนดอัตโนมัติ + - ส่งการแจ้งเตือนเมื่อมีการส่งต่อใหม่ + +#### 3.9.14 WorkflowEngineModule (New Core) + +- Entities: WorkflowDefinition, WorkflowInstance, WorkflowHistory +- Services: WorkflowEngineService, WorkflowDslService, WorkflowEventService +- Responsibility: จัดการ State Machine, Validate DSL, Execute Transitions + +#### 3.9.15 **JsonSchemaModule:** + +- **สถานะ:** Internal Module สำหรับจัดการ JSON schemas +- **หน้าที่:** Validate, transform, และ manage JSON data structures +- **Features:** + - JSON schema validation ด้วย AJV + - Schema versioning และ migration + - Dynamic schema generation + - Data transformation และ sanitization + +#### 3.9.16 **DetailsService:** + +- **สถานะ:** Shared Service สำหรับจัดการ details fields +- **หน้าที่:** Centralized service สำหรับ JSON details operations +- **Methods:** + - validateDetails(type: string, data: any): ValidationResult + - transformDetails(input: any, targetVersion: string): any + - sanitizeDetails(data: any): any + - getDefaultDetails(type: string): any + +### **3.10 สถาปัตยกรรมระบบ (System Architecture)** + +โครงสร้างโมดูล (Module Structure) อ้างถึง Backend Development Plan v1.4.5 + +### **3.11 กลยุทธ์ความทนทานและการจัดการข้อผิดพลาด (Resilience & Error Handling Strategy)** + +- **Circuit Breaker Pattern:** ใช้สำหรับ external service calls (Email, LINE, Elasticsearch) +- **Retry Mechanism:** ด้วย exponential backoff สำหรับ transient failures +- **Fallback Strategies:** Graceful degradation เมื่อบริการภายนอกล้มเหลว +- **Error Handling:** Error messages ต้องไม่เปิดเผยข้อมูล sensitive +- **Monitoring:** Centralized error monitoring และ alerting system + +### **3.12 FileStorageService (ปรับปรุงใหม่):** + +- **Virus Scanning:** Integrate ClamAV สำหรับ scan ไฟล์ที่อัปโหลดทั้งหมด +- **File Type Validation:** ใช้ white-list approach (PDF, DWG, DOCX, XLSX, ZIP) +- **File Size Limits:** 50MB ต่อไฟล์ +- **Security Measures:** + - เก็บไฟล์นอก web root + - Download ผ่าน authenticated endpoint เท่านั้น + - Download links มี expiration time (24 ชั่วโมง) + - File integrity checks (checksum) + - Access control checks ก่อนดาวน์โหลด + +### **3.13 เเทคโนโลยีที่ใช้ (Technology Stack)** + +| ส่วน | Library/Tool | หมายเหตุ | +| ----------------------- | ---------------------------------------------------- | -------------------------------------- | +| **Framework** | `@nestjs/core`, `@nestjs/common` | Core Framework | +| **Language** | `TypeScript` | ใช้ TypeScript ทั้งระบบ | +| **Database** | `MariaDB 10.11` | ฐานข้อมูลหลัก | +| **ORM** | `@nestjs/typeorm`, `typeorm` | 🗃️จัดการการเชื่อมต่อและ Query ฐานข้อมูล | +| **Validation** | `class-validator`, `class-transformer` | 📦ตรวจสอบและแปลงข้อมูลใน DTO | +| **Auth** | `@nestjs/jwt`, `@nestjs/passport`, `passport-jwt` | 🔐การยืนยันตัวตนด้วย JWT | +| **Authorization** | `casl` | 🔐จัดการสิทธิ์แบบ RBAC | +| **File Upload** | `multer` | 📁จัดการการอัปโหลดไฟล์ | +| **Search** | `@nestjs/elasticsearch` | 🔍สำหรับการค้นหาขั้นสูง | +| **Notification** | `nodemailer` | 📬ส่งอีเมลแจ้งเตือน | +| **Scheduling** | `@nestjs/schedule` | 📬สำหรับ Cron Jobs (เช่น แจ้งเตือน Deadline) | +| **Logging** | `winston` | 📊บันทึก Log ที่มีประสิทธิภาพ | +| **Testing** | `@nestjs/testing`, `jest`, `supertest` | 🧪ทดสอบ Unit, Integration และ E2E | +| **Documentation** | `@nestjs/swagger` | 🌐สร้าง API Documentation อัตโนมัติ | +| **Security** | `helmet`, `rate-limiter-flexible` | 🛡️เพิ่มความปลอดภัยให้ API | +| **Resilience** | `@nestjs/circuit-breaker` | 🔄 Circuit breaker pattern | +| **Caching** | `@nestjs/cache-manager`, `cache-manager-redis-store` | 💾 Distributed caching | +| **Security** | `helmet`, `csurf`, `rate-limiter-flexible` | 🛡️ Security enhancements | +| **Validation** | `class-validator`, `class-transformer` | ✅ Input validation | +| **Monitoring** | `@nestjs/monitoring`, `winston` | 📊 Application monitoring | +| **File Processing** | `clamscan` | 🦠 Virus scanning | +| **Cryptography** | `bcrypt`, `crypto` | 🔐 Password hashing และ checksums | +| **JSON Validation** | `ajv`, `ajv-formats` | 🎯 JSON schema validation | +| **JSON Processing** | `jsonpath`, `json-schema-ref-parser` | 🔧 JSON manipulation | +| **Data Transformation** | `class-transformer` | 🔄 Object transformation | +| **Compression** | `compression` | 📦 JSON compression | + +### **3.14 Security Testing:** + +- **Penetration Testing:** ทดสอบ OWASP Top 10 vulnerabilities +- **Security Audit:** Review code สำหรับ security flaws +- **Virus Scanning Test:** ทดสอบ file upload security +- **Rate Limiting Test:** ทดสอบ rate limiting functionality + +### **3.15 Performance Testing:** + +- **Load Testing:** ทดสอบด้วย realistic workloads +- **Stress Testing:** หา breaking points ของระบบ +- **Endurance Testing:** ทดสอบการทำงานต่อเนื่องเป็นเวลานาน + +### 🗄️**3.16 Backend State Management** + +Backend (NestJS) ควรเป็น **Stateless** (ไม่เก็บสถานะ) "State" ทั้งหมดจะถูกจัดเก็บใน MariaDB + +- **Request-Scoped State (สถานะภายใน Request เดียว):** + - **ปัญหา:** จะส่งต่อข้อมูล (เช่น User ที่ล็อกอิน) ระหว่าง Guard และ Service ใน Request เดียวกันได้อย่างไร? + - **วิธีแก้:** ใช้ **Request-Scoped Providers** ของ NestJS (เช่น AuthContextService) เพื่อเก็บข้อมูล User ปัจจุบันที่ได้จาก AuthGuard และให้ Service อื่น Inject ไปใช้ +- **Application-Scoped State (การ Caching):** + - **ปัญหา:** ข้อมูล Master (เช่น roles, permissions, organizations) ถูกเรียกใช้บ่อย + - **วิธีแก้:** ใช้ **Caching** (เช่น @nestjs/cache-manager) เพื่อ Caching ข้อมูลเหล่านี้ และลดภาระ Database + +### **3.17 Caching Strategy (ตามข้อ 6.4.2):** + +- **Master Data Cache:** Roles, Permissions, Organizations (TTL: 1 hour) +- **User Session Cache:** User permissions และ profile (TTL: 30 minutes) +- **Search Result Cache:** Frequently searched queries (TTL: 15 minutes) +- **File Metadata Cache:** Attachment metadata (TTL: 1 hour) +- **Cache Invalidation:** Clear cache on update/delete operations + +### **3.18 การไหลของข้อมูล (Data Flow)** + +#### **3.18.1 Main Flow:** + +1. Request: ผ่าน Nginx Proxy Manager -> NestJS Controller +2. **Rate Limiting:** RateLimitGuard ตรวจสอบ request limits +3. **Input Validation:** Validation Pipe ตรวจสอบและ sanitize inputs +4. Authentication: JWT Guard ตรวจสอบ Token และดึงข้อมูล User +5. Authorization: RBAC Guard ตรวจสอบสิทธิ์ +6. **Security Checks:** Virus scanning (สำหรับ file upload), XSS protection +7. Business Logic: Service Layer ประมวลผลตรรกะทางธุรกิจ +8. **Resilience:** Circuit breaker และ retry logic สำหรับ external calls +9. Data Access: Repository Layer ติดต่อกับฐานข้อมูล +10. **Caching:** Cache frequently accessed data +11. **Audit Log:** บันทึกการกระทำสำคัญ +12. Response: ส่งกลับไปยัง Frontend + +#### **3.18.2 Workflow Data Flow:** + +1. User สร้างเอกสาร → เลือก routing template +2. System สร้าง routing instances ตาม template +3. สำหรับแต่ละ routing step: + + - กำหนด due date (จาก expected_days) + - ส่ง notification ไปยังองค์กรผู้รับ + - อัพเดทสถานะเป็น SENT + +4. เมื่อองค์กรผู้รับดำเนินการ: + + - อัพเดทสถานะเป็น ACTIONED/FORWARDED/REPLIED + - บันทึก processed_by และ processed_at + - ส่ง notification ไปยังขั้นตอนต่อไป (ถ้ามี) + +5. เมื่อครบทุกขั้นตอน → อัพเดทสถานะเอกสารเป็น COMPLETED + +#### **3.18.3 JSON Details Processing Flow:** + +1. **Receive Request** → Get JSON data from client +2. **Schema Validation** → Validate against predefined schema +3. **Data Sanitization** → Sanitize and transform data +4. **Version Check** → Handle schema version compatibility +5. **Storage** → Store validated JSON in database +6. **Retrieval** → Retrieve and transform on demand + +### 📊**3.19 Monitoring & Observability (ตามข้อ 6.8)** + +#### **Application Monitoring:** + +- **Health Checks:** `/health` endpoint สำหรับ load balancer +- **Metrics Collection:** Response times, error rates, throughput +- **Distributed Tracing:** สำหรับ request tracing across services +- **Log Aggregation:** Structured logging ด้วย JSON format +- **Alerting:** สำหรับ critical errors และ performance degradation + +#### **Business Metrics:** + +- จำนวน documents created ต่อวัน +- Workflow completion rates +- User activity metrics +- System utilization rates +- Search query performance + +#### **Performance Targets:** + +- API Response Time: + - Simple CRUD: < 100ms + - Complex Search: < 500ms + - File Processing: < 2s +- File Upload Performance: < 30 seconds สำหรับไฟล์ 50MB +- Cache Hit Ratio: > 80% + +### **3.20 Logging Strategy for QNAP Environment** + +เนื่องจากระบบรันบน QNAP Container Station ซึ่งอาจมีข้อจำกัดเรื่อง Disk I/O และ Storage: + +- Log Levels: ให้กำหนด Log Level ของ Production เป็น WARN หรือ ERROR เป็นหลัก +- Info Logs: ใช้ INFO เฉพาะ Flow ที่สำคัญทางธุรกิจเท่านั้น (เช่น Workflow State Change, Login Success/Fail, File Upload Commit) +- Console Logging: หลีกเลี่ยง console.log ปริมาณมาก (Verbose) ให้ใช้ Winston Logger ที่ Config ให้จัดการ Rotation และ Format ได้ดีกว่า +- Disable Debug: ปิด Debug Log ทั้งหมดใน Production Mode + +## 🖥️ **4. ฟรอนต์เอนด์ (Next.js) - Implementation Details** + +### **4.1 State Management & Offline Support** + +#### **4.1.1 Auto-Save Drafts** + +ใช้ **React Hook Form** ร่วมกับ **persist** mechanism สำหรับฟอร์มที่มีขนาดใหญ่ (เช่น RFA, Correspondence): + +```typescript +// hooks/useAutoSaveForm.ts +export const useAutoSaveForm = (formKey: string, defaultValues: any) => { + const { register, watch, setValue } = useForm({ defaultValues }); + + // Auto-save เมื่อ form เปลี่ยนแปลง + useEffect(() => { + const subscription = watch((value) => { + localStorage.setItem(`draft-${formKey}`, JSON.stringify(value)); + }); + return () => subscription.unsubscribe(); + }, [watch, formKey]); + + // Load draft เมื่อ component mount + useEffect(() => { + const draft = localStorage.getItem(`draft-${formKey}`); + if (draft) { + const parsed = JSON.parse(draft); + Object.keys(parsed).forEach((key) => { + setValue(key, parsed[key]); + }); + } + }, [formKey, setValue]); + + return { register }; +}; +``` + +#### **4.1.2 Silent Refresh Strategy** + +ใช้ React Query สำหรับจัดการ token refresh อัตโนมัติ + +```typescript +// lib/api/client.ts +const apiClient = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, +}); + +// React Query จะจัดการ token refresh อัตโนมัติผ่าน interceptors +``` + +### **4.2 Dynamic Form Generator** + +เพื่อรองรับ JSON Schema หลากหลายรูปแบบ ให้สร้าง Component กลางที่รับ Schema แล้ว Gen Form ออกมา (ลดการแก้ Code บ่อยๆ) + +```tsx +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useQuery } from '@tanstack/react-query'; + +interface DynamicFormProps { + schemaName: string; + onSubmit: (data: any) => void; +} + +export function DynamicForm({ schemaName, onSubmit }: DynamicFormProps) { + // Fetch JSON Schema from Backend + const { data: schema } = useQuery({ + queryKey: ['json-schema', schemaName], + queryFn: () => jsonSchemaService.getByName(schemaName), + }); + + // Generate Zod schema from JSON Schema + const zodSchema = useMemo(() => { + if (!schema) return null; + return generateZodSchemaFromJsonSchema(schema.schema_definition); + }, [schema]); + + const form = useForm({ + resolver: zodResolver(zodSchema!), + }); + + if (!schema) return ; + + return ( +
+ + {Object.entries(schema.schema_definition.properties).map( + ([key, prop]: [string, any]) => ( + ( + + {prop.title || key} + + {renderFieldByType(prop.type, field)} + + + + )} + /> + ) + )} + + + + ); +} + +// Helper function to render different field types +function renderFieldByType(type: string, field: any) { + switch (type) { + case 'string': + return ; + case 'number': + return ; + case 'boolean': + return ; + // Add more types as needed + default: + return ; + } +} +``` + +### **4.3 Mobile Responsiveness (Card View)** + +ตารางข้อมูล (`DataTable`) ต้องมีความฉลาดในการแสดงผล: Mobile: Card View, Desktop: Table View + +```tsx +export function ResponsiveTable({ data }: { data: Correspondence[] }) { + return ( + <> + {/* Desktop Table */} +
+ + + + เลขที่เอกสาร + เรื่อง + สถานะ + + + + {data.map((item) => ( + + {item.doc_number} + {item.title} + + {item.status} + + + ))} + +
+
+ + {/* Mobile Card View */} +
+ {data.map((item) => ( + + +
+
+ เลขที่เอกสาร +
+
{item.doc_number}
+
เรื่อง
+
{item.title}
+ {item.status} +
+
+
+ ))} +
+ + ); +} +``` + +### **4.4 Optimistic Updates** + +ใช้ความสามารถของ **TanStack Query** (`onMutate`) เพื่ออัปเดต UI ทันที (เช่น เปลี่ยนสถานะจาก "รออ่าน" เป็น "อ่านแล้ว") แล้วค่อยส่ง Request ไป Server ถ้า Failed ค่อย Rollback + +### **4.5 แนวทางการพัฒนาโค้ด (Code Implementation Guidelines)** + +- ใช้ **early returns** เพื่อความชัดเจน +- ใช้คลาสของ **TailwindCSS** ในการกำหนดสไตล์เสมอ +- ควรใช้ class: syntax แบบมีเงื่อนไข (หรือ utility clsx) มากกว่าการใช้ ternary operators ใน class strings +- ใช้ **const arrow functions** สำหรับ components และ handlers +- Event handlers ให้ขึ้นต้นด้วย handle... (เช่น handleClick, handleSubmit) +- รวมแอตทริบิวต์สำหรับการเข้าถึง (accessibility) ด้วย: + tabIndex="0", aria-label, onKeyDown, ฯลฯ +- ตรวจสอบให้แน่ใจว่าโค้ดทั้งหมด **สมบูรณ์**, **ผ่านการทดสอบ**, และ **ไม่ซ้ำซ้อน (DRY)** +- ต้อง import โมดูลที่จำเป็นต้องใช้อย่างชัดเจนเสมอ + +### **4.6 UI/UX ด้วย React** + +- ใช้ **semantic HTML** +- ใช้คลาสของ **Tailwind** ที่รองรับ responsive (sm:, md:, lg:) +- รักษาลำดับชั้นของการมองเห็น (visual hierarchy) ด้วยการใช้ typography และ spacing +- ใช้ **Shadcn** components (Button, Input, Card, ฯลฯ) เพื่อ UI ที่สอดคล้องกัน +- ทำให้ components มีขนาดเล็กและมุ่งเน้นการทำงานเฉพาะอย่าง +- ใช้ utility classes สำหรับการจัดสไตล์อย่างรวดเร็ว (spacing, colors, text, ฯลฯ) +- ตรวจสอบให้แน่ใจว่าสอดคล้องกับ **ARIA** และใช้ semantic markup + +### **4.7 การตรวจสอบฟอร์มและข้อผิดพลาด (Form Validation & Errors)** + +- ใช้ไลบรารีฝั่ง client เช่น zod และ react-hook-form +- แสดงข้อผิดพลาดด้วย **alert components** หรือข้อความ inline +- ต้องมี labels, placeholders, และข้อความ feedback + +### **4.8 Error Handling & Resilience (Frontend)** + +#### **4.8.1 Global Error Handling with React Query** + +ใช้ **React Query** Error Boundaries สำหรับจัดการ errors แบบรวมศูนย์: + +```typescript +// app/providers.tsx +export function QueryProvider({ children }: { children: React.ReactNode }) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + staleTime: 5 * 60 * 1000, // 5 minutes + }, + mutations: { + onError: (error) => { + // Global mutation error handling + toast.error('Operation failed'); + }, + }, + }, + }); + + return ( + {children} + ); +} +``` + +### **🧪4.9 Frontend Testing** + +เราจะใช้ **React Testing Library (RTL)** สำหรับการทดสอบ Component และ **Playwright** สำหรับ E2E: + +- **Unit Tests (การทดสอบหน่วยย่อย):** + - **เครื่องมือ:** Vitest + RTL + - **เป้าหมาย:** ทดสอบ Component ขนาดเล็ก (เช่น Buttons, Inputs) หรือ Utility functions +- **Integration Tests (การทดสอบการบูรณาการ):** + - **เครื่องมือ:** RTL + **Mock Service Worker (MSW)** + - **เป้าหมาย:** ทดสอบว่า Component หรือ Page ทำงานกับ API (ที่จำลองขึ้น) ได้ถูกต้อง + - **เทคนิค:** ใช้ MSW เพื่อจำลอง NestJS API และทดสอบว่า Component แสดงผลข้อมูลจำลองได้ถูกต้องหรือไม่ (เช่น ทดสอบหน้า Dashboard [cite: 5.3] ที่ดึงข้อมูลจาก v_user_tasks) +- **E2E (End-to-End) Tests:** + - **เครื่องมือ:** **Playwright** + - **เป้าหมาย:** ทดสอบ User Flow ทั้งระบบโดยอัตโนมัติ (เช่น ล็อกอิน -> สร้าง RFA -> ตรวจสอบ Workflow Visualization [cite: 5.6]) + +### 🗄️4.10 Frontend State Management (ปรับปรุง) + +สำหรับ Next.js App Router เราจะใช้ State Management แบบ Simplified โดยแบ่งเป็น 3 ระดับหลัก: + +#### **4.10.1 Server State - TanStack Query** + +**ใช้สำหรับข้อมูลจาก API:** + +```tsx +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; + +// Fetch data +export function useCorrespondences(projectId: string) { + return useQuery({ + queryKey: ['correspondences', projectId], + queryFn: () => correspondenceService.getAll(projectId), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} + +// Mutation with optimistic update +export function useCreateCorrespondence() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: correspondenceService.create, + onMutate: async (newCorrespondence) => { + // Optimistic update + await queryClient.cancelQueries({ queryKey: ['correspondences'] }); + const previous = queryClient.getQueryData(['correspondences']); + + queryClient.setQueryData(['correspondences'], (old: any) => [ + ...old, + newCorrespondence, + ]); + + return { previous }; + }, + onError: (err, newCorrespondence, context) => { + // Rollback on error + queryClient.setQueryData(['correspondences'], context?.previous); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['correspondences'] }); + }, + }); +} +``` + +#### **4.10.2 Form State - React Hook Form + Zod** + +```tsx +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; + +// Schema Definition +const formSchema = z.object({ + title: z.string().min(1, 'กรุณาระบุหัวเรื่อง').max(500), + project_id: z.string().uuid('กรุณาเลือกโปรเจกต์'), + type_id: z.string().uuid('กรุณาเลือกประเภทเอกสาร'), +}); + +type FormData = z.infer; + +// Form Component +export function CorrespondenceForm() { + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + title: '', + project_id: '', + type_id: '', + }, + }); + + const onSubmit = async (data: FormData) => { + await createCorrespondence(data); + }; + + return ( +
+ + ( + + หัวเรื่อง + + + + + + )} + /> + + + + ); +} +``` + +#### **4.10.3 UI State - Zustand** + +```tsx +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +// Draft Store (with localStorage persistence) +interface DraftStore { + drafts: Record; + saveDraft: (formKey: string, data: any) => void; + loadDraft: (formKey: string) => any; + clearDraft: (formKey: string) => void; +} + +export const useDraftStore = create()( + persist( + (set, get) => ({ + drafts: {}, + saveDraft: (formKey, data) => + set((state) => ({ + drafts: { ...state.drafts, [formKey]: data }, + })), + loadDraft: (formKey) => get().drafts[formKey], + clearDraft: (formKey) => + set((state) => { + const { [formKey]: _, ...rest } = state.drafts; + return { drafts: rest }; + }), + }), + { name: 'correspondence-drafts' } + ) +); +``` + +### 4.11 State Management Best Practices + +#### **4.11.1 หลักการพื้นฐาน:** + +- **Server State ≠ Client State:** แยก state ตามแหล่งที่มาให้ชัดเจน +- **ใช้ Tools ให้ถูกหน้าที่:** แต่ละ tool ใช้แก้ปัญหาที่เฉพาะเจาะจง +- **Avoid Over-engineering:** เริ่มจาก useState ก่อน แล้วค่อยขยายตามความจำเป็น + +#### **4.11.2 Decision Framework:** + +- **Server State:** ใช้ React Query หรือ SWR +- **Form State:** ใช้ React Hook Form หรือ Formik +- **UI State:** ใช้ useState/useReducer +- **Global App State:** ใช้ React Query หรือ Context API + +#### **4.11.3 Performance Considerations:** + +- ใช้ `useMemo` และ `useCallback` สำหรับ expensive computations +- ใช้ React Query's `select` option สำหรับ derived data +- หลีกเลี่ยง unnecessary re-renders ด้วย proper dependency arrays + +### **4.12 File Upload Component (Drag & Drop)** + +```tsx +import { useCallback } from 'react'; +import { useDropzone } from 'react-dropzone'; +import { Upload, X } from 'lucide-react'; + +interface FileUploadZoneProps { + onUpload: (files: File[]) => void; + maxFiles?: number; + maxSize?: number; + acceptedTypes?: string[]; +} + +export function FileUploadZone({ + onUpload, + maxFiles = 10, + maxSize = 50 * 1024 * 1024, // 50MB + acceptedTypes = ['.pdf', '.dwg', '.docx', '.xlsx', '.zip'], +}: FileUploadZoneProps) { + const onDrop = useCallback( + (acceptedFiles: File[]) => { + onUpload(acceptedFiles); + }, + [onUpload] + ); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + maxFiles, + maxSize, + accept: acceptedTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}), + }); + + return ( +
+ + +

+ {isDragActive + ? 'วางไฟล์ที่นี่...' + : 'ลากไฟล์มาวางที่นี่ หรือคลิกเพื่อเลือกไฟล์'} +

+

+ รองรับ: {acceptedTypes.join(', ')} (สูงสุด {maxFiles} ไฟล์,{' '} + {maxSize / 1024 / 1024}MB/ไฟล์) +

+
+ ); +} +``` + +## 🔐 **5. Security & Access Control (Full Stack Integration)** + +### **5.1 CASL Integration (Shared Ability)** + +- **Backend:** ใช้ CASL กำหนด Permission Rule +- **Frontend:** ให้ดึง Rule (JSON) จาก Backend มา Load ใส่ `@casl/react` เพื่อให้ Logic การ Show/Hide ปุ่ม ตรงกัน 100% + +### **5.2 Maintenance Mode** + +เพิ่ม Middleware (ทั้ง NestJS และ Next.js) เพื่อตรวจสอบ Flag ใน Redis: + +- ถ้า `MAINTENANCE_MODE = true` +- **API:** Return `503 Service Unavailable` (ยกเว้น Admin IP) +- **Frontend:** Redirect ไปหน้า `/maintenance` + +### **5.3 Idempotency Client** + +สร้าง Axios Interceptor เพื่อ Generate `Idempotency-Key` สำหรับ POST/PUT/DELETE requests ทุกครั้ง + +```typescript +// lib/api/client.ts +import { v4 as uuidv4 } from 'uuid'; + +apiClient.interceptors.request.use((config) => { + if (['post', 'put', 'delete'].includes(config.method)) { + config.headers['Idempotency-Key'] = uuidv4(); + } + return config; +}); +``` + +### **5.4 RBAC และการควบคุมสิทธิ์ (RBAC & Permission Control)** + +ใช้ Decorators เพื่อบังคับใช้สิทธิ์การเข้าถึง โดยอ้างอิงสิทธิ์จากตาราง permissions + +```typescript +@RequirePermission('rfas.respond') // ต้องตรงกับ 'permission_code' +@Put(':id') +updateRFA(@Param('id') id: string) { + return this.rfaService.update(id); +} +``` + +#### **5.4.1 Roles (บทบาท)** + +- **Superadmin**: ไม่มีข้อจำกัดใดๆ [cite: 4.3] +- **Admin**: มีสิทธิ์เต็มที่ในองค์กร [cite: 4.3] +- **Document Control**: เพิ่ม/แก้ไข/ลบ เอกสารในองค์กร [cite: 4.3] +- **Editor**: สามารถ เพิ่ม/แก้ไข เอกสารที่กำหนด [cite: 4.3] +- **Viewer**: สามารถดู เอกสาร [cite: 4.3] + +#### **5.4.2 ตัวอย่าง Permissions (จากตาราง permissions)** + +- rfas.view, rfas.create, rfas.respond, rfas.delete +- drawings.view, drawings.upload, drawings.delete +- corr.view, corr.manage +- transmittals.manage +- cirs.manage +- project_parties.manage + +การจับคู่ระหว่าง roles และ permissions **เริ่มต้น** จะถูก seed ผ่านสคริปต์ (ดังที่เห็นในไฟล์ SQL)**อย่างไรก็ตาม AuthModule/UserModule ต้องมี API สำหรับ Admin เพื่อสร้าง Role ใหม่และกำหนดสิทธิ์ (Permissions) เพิ่มเติมได้ในภายหลัง** [cite: 4.3] + +## 📊 **6. Notification & Background Jobs** + +### **6.1 Digest Notification** + +ห้ามส่ง Email ทันทีที่เกิด Event ให้: + +1. Push Event ลง Queue (Redis/BullMQ) +2. มี Processor รอเวลา (เช่น 5 นาที) เพื่อ Group Events ที่คล้ายกัน (เช่น "คุณมีเอกสารรออนุมัติ 5 ฉบับ") +3. ส่ง Email เดียว (Digest) เพื่อลด Spam + +## 🔗 **7. แนวทางการบูรณาการ Full Stack (Full Stack Integration Guidelines)** + +| Aspect (แง่มุม) | Backend (NestJS) | Frontend (NextJS) | UI Layer (Tailwind/Shadcn) | +| :----------------------- | :------------------------- | :---------------------------- | :------------------------------------- | +| API | REST / GraphQL Controllers | API hooks ผ่าน fetch/axios/SWR | Components ที่รับข้อมูล | +| Validation (การตรวจสอบ) | class-validator DTOs | zod / react-hook-form | สถานะของฟอร์ม/input ใน Shadcn | +| Auth (การยืนยันตัวตน) | Guards, JWT | NextAuth / cookies | สถานะ UI ของ Auth (loading, signed in) | +| Errors (ข้อผิดพลาด) | Global filters | Toasts / modals | Alerts / ข้อความ feedback | +| Testing (การทดสอบ) | Jest (unit/e2e) | Vitest / Playwright | Visual regression | +| Styles (สไตล์) | Scoped modules (ถ้าจำเป็น) | Tailwind / Shadcn | Tailwind utilities | +| Accessibility (การเข้าถึง) | Guards + filters | ARIA attributes | Semantic HTML | + +## 🗂️ **8. ข้อตกลงเฉพาะสำหรับ DMS (LCBP3-DMS)** + +ส่วนนี้ขยายแนวทาง FullStackJS ทั่วไปสำหรับโปรเจกต์ **LCBP3-DMS** โดยมุ่งเน้นไปที่เวิร์กโฟลว์การอนุมัติเอกสาร (Correspondence, RFA, Drawing, Contract, Transmittal, Circulation) + +### 🧾**8.1 มาตรฐาน AuditLog (AuditLog Standard)** + +บันทึกการดำเนินการ CRUD และการจับคู่ทั้งหมดลงในตาราง audit_logs + +| Field (ฟิลด์) | Type (จาก SQL) | Description (คำอธิบาย) | +| :----------- | :------------- | :----------------------------------------------- | +| audit_id | BIGINT | Primary Key | +| user_id | INT | ผู้ใช้ที่ดำเนินการ (FK -> users) | +| action | VARCHAR(100) | rfa.create, correspondence.update, login.success | +| entity_type | VARCHAR(50) | ชื่อตาราง/โมดูล เช่น 'rfa', 'correspondence' | +| entity_id | VARCHAR(50) | Primary ID ของระเบียนที่ได้รับผลกระทบ | +| details_json | JSON | ข้อมูลบริบท (เช่น ฟิลด์ที่มีการเปลี่ยนแปลง) | +| ip_address | VARCHAR(45) | IP address ของผู้ดำเนินการ | +| user_agent | VARCHAR(255) | User Agent ของผู้ดำเนินการ | +| created_at | TIMESTAMP | Timestamp (UTC) | + +### 📂**8.2 การจัดการไฟล์ (File Handling)** + +#### **8.2.1 มาตรฐานการอัปโหลดไฟล์ (File Upload Standard)** + +- **Security-First Approach:** การอัปโหลดไฟล์ทั้งหมดจะถูกจัดการโดย FileStorageService ที่มี security measures ครบถ้วน +- ไฟล์จะถูกเชื่อมโยงไปยัง Entity ที่ถูกต้องผ่าน **ตารางเชื่อม (Junction Tables)** เท่านั้น: + - correspondence_attachments (เชื่อม Correspondence กับ Attachments) + - circulation_attachments (เชื่อม Circulation กับ Attachments) + - shop_drawing_revision_attachments (เชื่อม Shop Drawing Revision กับ Attachments) + - contract_drawing_attachments (เชื่อม Contract Drawing กับ Attachments) +- เส้นทางจัดเก็บไฟล์ (Upload path): อ้างอิงจาก Requirement 2.1 คือ /share/dms-data [cite: 2.1] โดย FileStorageService จะสร้างโฟลเดอร์ย่อยแบบรวมศูนย์ (เช่น /share/dms-data/uploads/{YYYY}/{MM}/[stored_filename]) +- ประเภทไฟล์ที่อนุญาต: pdf, dwg, docx, xlsx, zip (ผ่าน white-list validation) +- ขนาดสูงสุด: **50 MB** +- จัดเก็บนอก webroot +- ให้บริการไฟล์ผ่าน endpoint ที่ปลอดภัย /files/:attachment_id/download + +#### **8.2.2 Security Controls สำหรับ File Access:** + +การเข้าถึงไฟล์ไม่ใช่การเข้าถึงโดยตรง endpoint /files/:attachment_id/download จะต้อง: + +1. ค้นหาระเบียน attachment +2. ตรวจสอบว่า attachment_id นี้ เชื่อมโยงกับ Entity ใด (เช่น correspondence, circulation, shop_drawing_revision, contract_drawing) ผ่านตารางเชื่อม +3. ตรวจสอบว่าผู้ใช้มีสิทธิ์ (permission) ในการดู Entity ต้นทางนั้นๆ หรือไม่ +4. ตรวจสอบ download token expiration (24 ชั่วโมง) +5. บันทึก audit log การดาวน์โหลด + +### 🔟**8.3 การจัดการเลขที่เอกสาร (Document Numbering) [cite: 3.10]** + +- **เป้าหมาย:** สร้างเลขที่เอกสาร (เช่น correspondence_number) โดยอัตโนมัติ ตามรูปแบบที่กำหนด +- **ตรรกะการนับ:** การนับ Running number (SEQ) จะนับแยกตาม Key: **Project + Originator Organization + Document Type + Year** +- **ตาราง SQL (Updated):** + - `document_number_formats`: เก็บ Template String (เช่น `{CONTRACT}-{TYPE}-{DISCIPLINE}-{SEQ:4}`) + - `document_number_counters`: **Primary Key เปลี่ยนเป็น Composite Key ใหม่:** `(project_id, originator_id, type_id, discipline_id, current_year)` เพื่อรองรับการรันเลขแยกตามสาขา +- **การทำงาน:** + - Service ต้องรองรับการ Resolve Token พิเศษ เช่น `{SUBTYPE_NUM}` ที่ต้องไป Join กับตาราง `correspondence_sub_types` + - DocumentNumberingModule จะให้บริการ DocumentNumberingService + - เมื่อ CorrespondenceModule ต้องการสร้างเอกสารใหม่, มันจะเรียก documentNumberingService.generateNextNumber(...) + - Service นี้จะใช้ **Redis distributed locking** แทน stored procedure ซึ่งจะจัดการ Database Transaction และ Row Locking ภายใน Application Layer เพื่อรับประกันการป้องกัน Race Condition + - มี retry mechanism และ fallback strategies + +### 📊**8.4 การรายงานและการส่งออก (Reporting & Exports)** + +#### **8.4.1 วิวสำหรับการรายงาน (Reporting Views) (จาก SQL)** + +การรายงานควรสร้างขึ้นจาก Views ที่กำหนดไว้ล่วงหน้าในฐานข้อมูลเป็นหลัก: + +- v_current_correspondences: สำหรับ revision ปัจจุบันทั้งหมดของเอกสารที่ไม่ใช่ RFA +- v_current_rfas: สำหรับ revision ปัจจุบันทั้งหมดของ RFA และข้อมูล master +- v_contract_parties_all: สำหรับการตรวจสอบความสัมพันธ์ของ project/contract/organization +- v_user_tasks: สำหรับ Dashboard "งานของฉัน" +- v_audit_log_details: สำหรับ Activity Feed + +Views เหล่านี้ทำหน้าที่เป็นแหล่งข้อมูลหลักสำหรับการรายงานฝั่งเซิร์ฟเวอร์และการส่งออกข้อมูล + +#### **8.4.2 กฎการส่งออก (Export Rules)** + +- Export formats: CSV, Excel, PDF. +- จัดเตรียมมุมมองสำหรับพิมพ์ (Print view). +- รวมลิงก์ไปยังต้นทาง (เช่น /rfas/:id). + +## 🧮 **9. ฟรอนต์เอนด์: รูปแบบ DataTable และฟอร์ม (Frontend: DataTable & Form Patterns)** + +### **9.1 DataTable (Server‑Side)** + +- Endpoint: /api/{module}?page=1&pageSize=20&sort=...&filter=... +- ต้องรองรับ: การแบ่งหน้า (pagination), การเรียงลำดับ (sorting), การค้นหา (search), การกรอง (filters) +- แสดง revision ล่าสุดแบบ inline เสมอ (สำหรับ RFA/Drawing) + +### **9.2 มาตรฐานฟอร์ม (Form Standards)** + +- ใช้ **React Hook Form** เป็นมาตรฐานสำหรับฟอร์มทั้งหมด +- ใช้ **Zod** สำหรับ schema validation ทั้งฝั่ง client และ server +- ต้องมีการใช้งาน Dropdowns แบบขึ้นต่อกัน (Dependent dropdowns) (ตามที่สคีมารองรับ) ด้วย React Query สำหรับ data fetching และ React Hook Form สำหรับ state management: + - Project → Contract Drawing Volumes + - Contract Drawing Category → Sub-Category + - RFA (ประเภท Shop Drawing) → Shop Drawing Revisions ที่เชื่อมโยงได้ +- **File Upload Security:** ต้องรองรับ **Multi-file upload (Drag-and-Drop)** ด้วย React Hook Form integration [cite: 5.7] พร้อม virus scanning feedback +- **File Type Indicators:** UI ต้องอนุญาตให้ผู้ใช้กำหนดว่าไฟล์ใดเป็น **"เอกสารหลัก"** หรือ "เอกสารแนบประกอบ" [cite: 5.7] พร้อมแสดง file type icons +- **Security Feedback:** แสดง security warnings สำหรับ file types ที่เสี่ยงหรือ files ที่ fail virus scan +- ส่ง (Submit) ผ่าน API พร้อม feedback แบบ toast + +### **9.3 ข้อกำหนด Component เฉพาะ (Specific UI Requirements)** + +- **Dashboard - My Tasks:** ต้องพัฒนา Component ตาราง "งานของฉัน" (My Tasks)ซึ่งดึงข้อมูลงานที่ผู้ใช้ล็อกอินอยู่ต้องรับผิดชอบ (Main/Action) จาก v_user_tasks [cite: 5.3] +- **Workflow Visualization:** ต้องพัฒนา Component สำหรับแสดงผล Workflow (โดยเฉพาะ RFA)ที่แสดงขั้นตอนทั้งหมดเป็นลำดับ โดยขั้นตอนปัจจุบัน (active) เท่านั้นที่ดำเนินการได้ และขั้นตอนอื่นเป็น disabled [cite: 5.6] ต้องมีตรรกะสำหรับ Admin ในการ override หรือย้อนกลับขั้นตอนได้ [cite: 5.6] +- **Admin Panel:** ต้องมีหน้า UI สำหรับ Superadmin/Admin เพื่อจัดการข้อมูลหลัก (Master Data [cite: 4.5]), การเริ่มต้นใช้งาน (Onboarding [cite: 4.6]), และ **รูปแบบเลขที่เอกสาร (Numbering Formats [cite: 3.10])** +- **Security Dashboard:** แสดง security metrics และ audit logs สำหรับ administrators + +## 🧭 **10. แดชบอร์ดและฟีดกิจกรรม (Dashboard & Activity Feed)** + +### **10.1 การ์ดบนแดชบอร์ด (Dashboard Cards)** + +- แสดง Correspondences, RFAs, Circulations, Shop Drawing Revision ล่าสุด +- รวมสรุป KPI (เช่น "RFAs ที่รอการอนุมัติ", "Shop Drawing ที่รอการอนุมัติ") [cite: 5.3] +- รวมลิงก์ด่วนไปยังโมดูลต่างๆ +- **Security Metrics:** แสดงจำนวน files scanned, security incidents, failed login attempts + +### **10.2 ฟีดกิจกรรม (Activity Feed)** + +- แสดงรายการ v_audit_log_details ล่าสุด (10 รายการ) ที่เกี่ยวข้องกับผู้ใช้ +- รวม security-related activities (failed logins, permission changes) + +```typescript +// ตัวอย่าง API response +[ + { + user: 'editor01', + action: 'Updated RFA (LCBP3-RFA-001)', + time: '2025-11-04T09:30Z', + }, + { + user: 'system', + action: 'Virus scan completed - 0 threats found', + time: '2025-11-04T09:25Z', + }, +]; +``` + +## 🛡️ **11. ข้อกำหนดที่ไม่ใช่ฟังก์ชันการทำงาน (Non-Functional Requirements)** + +ส่วนนี้สรุปข้อกำหนด Non-Functional จาก requirements.md เพื่อให้ทีมพัฒนาทาน + +- **Audit Log [cite: 6.1]:** ทุกการกระทำที่สำคัญ (C/U/D) ต้องถูกบันทึกใน audit_logs +- **Performance [cite: 6.4]:** ต้องใช้ Caching สำหรับข้อมูลที่เรียกบ่อย และใช้ Pagination +- **Security [cite: 6.5]:** ต้องมี Rate Limiting และจัดการ Secret ผ่าน docker-compose.yml (ไม่ใช่ .env) +- **File Security [cite: 3.9.6]:** ต้องมี virus scanning, file type validation, access controls +- **Resilience [cite: 6.5.3]:** ต้องมี circuit breaker, retry mechanisms, graceful degradation +- **Backup & Recovery [cite: 6.6]:** ต้องมีแผนสำรองข้อมูลทั้ง Database (MariaDB) และ File Storage (/share/dms-data) อย่างน้อยวันละ 1 ครั้ง +- **Notification Strategy [cite: 6.7]:** ระบบแจ้งเตือน (Email/Line) ต้องถูก Trigger เมื่อมีเอกสารใหม่ส่งถึง, มีการมอบหมายงานใหม่ (Circulation), หรือ (ทางเลือก) เมื่องานเสร็จ/ใกล้ถึงกำหนด +- **Monitoring [cite: 6.8]:** ต้องมี health checks, metrics collection, alerting + +## ✅ **12. มาตรฐานที่นำไปใช้แล้ว (จาก SQL v1.4.0) (Implemented Standards (from SQL v1.4.0))** + +ส่วนนี้ยืนยันว่าแนวทางปฏิบัติที่ดีที่สุดต่อไปนี้เป็นส่วนหนึ่งของการออกแบบฐานข้อมูลอยู่แล้ว และควรถูกนำไปใช้ประโยชน์ ไม่ใช่สร้างขึ้นใหม่ + +- ✅ **Soft Delete:** นำไปใช้แล้วผ่านคอลัมน์ deleted_at ในตารางสำคัญ (เช่น correspondences, rfas, project_parties) ตรรกะการดึงข้อมูลต้องกรอง deleted_at IS NULL +- ✅ **Database Indexes:** สคีมาได้มีการทำ index ไว้อย่างหนักหน่วงบน foreign keys และคอลัมน์ที่ใช้ค้นหาบ่อย (เช่น idx_rr_rfa, idx_cor_project, idx_cr_is_current) เพื่อประสิทธิภาพ +- ✅ **โครงสร้าง RBAC:** มีระบบ users, roles, permissions, user_roles, และ user_project_roles ที่ครอบคลุมอยู่แล้ว +- ✅ **Data Seeding:** ข้อมูล Master (roles, permissions, organization_roles, initial users, project parties) ถูกรวมอยู่ในสคริปต์สคีมาแล้ว +- ✅ **Application-level Locking:** ใช้ Redis distributed lock แทน stored procedure +- ✅ **File Security:** Virus scanning, file type validation, access control +- ✅ **Resilience Patterns:** Circuit breaker, retry, fallback mechanisms +- ✅ **Security Measures:** Input validation, rate limiting, security headers +- ✅ **Monitoring:** Health checks, metrics collection, distributed tracing + +## 🧩 **13. การปรับปรุงที่แนะนำ (สำหรับอนาคต) (Recommended Enhancements (Future))** + +- ✅ สร้าง Background job (โดยใช้ **n8n** เพื่อเชื่อมต่อกับ **Line** [cite: 2.7] และ/หรือใช้สำหรับการแจ้งเตือน RFA ที่ใกล้ถึงกำหนด due_date [cite: 6.7]) +- ✅ เพิ่ม job ล้างข้อมูลเป็นระยะสำหรับ attachments ที่ไม่ถูกเชื่อมโยงกับ Entity ใดๆ เลย (ไฟล์กำพร้า) +- 🔄 **AI-Powered Document Classification:** ใช้ machine learning สำหรับ automatic document categorization +- 🔄 **Advanced Analytics:** Predictive analytics สำหรับ workflow optimization +- 🔄 **Mobile App:** Native mobile application สำหรับ field workers +- 🔄 **Blockchain Integration:** สำหรับ document integrity verification ที่ต้องการความปลอดภัยสูงสุด + +## ✅ **14. Summary Checklist for Developers** + +ก่อนส่ง PR (Pull Request) นักพัฒนาต้องตรวจสอบหัวข้อต่อไปนี้: + +- [ ] **Security:** ไม่มี Secrets ใน Code, ใช้ `docker-compose.override.yml` แล้ว +- [ ] **Concurrency:** ใช้ Optimistic Lock ใน Entity ที่เสี่ยง Race Condition แล้ว +- [ ] **Idempotency:** API รองรับ Idempotency Key แล้ว +- [ ] **File Upload:** ใช้ Flow Two-Phase (Temp -> Perm) แล้ว +- [ ] **Mobile:** หน้าจอแสดงผลแบบ Card View บนมือถือได้ถูกต้อง +- [ ] **Performance:** สร้าง Index สำหรับ JSON Virtual Columns แล้ว (ถ้ามี), ใช้ useMemo/useCallback ที่เหมาะสม +- [ ] **No Over-engineering:** ไม่ใช้ state management libraries เกินความจำเป็น +- [ ] **State Management:** ใช้ React Query สำหรับ server state, React Hook Form สำหรับ forms +- [ ] **Error Handling:** มี error boundaries และ proper error states +- [ ] **Type Safety:** มี proper TypeScript types สำหรับทั้งหมด state + +--- + +## 📋 **15. Summary of Key Changes from Previous Version** + +### **Security Enhancements:** + +1. **File Upload Security** - Virus scanning, file type validation, access controls +2. **Input Validation** - OWASP Top 10 protection, XSS/CSRF prevention +3. **Rate Limiting** - Comprehensive rate limiting strategy +4. **Secrets Management** - Secure handling of sensitive configuration + +### **Architecture Improvements:** + +1. **Document Numbering** - Changed from Stored Procedure to Application-level Locking +2. **Resilience Patterns** - Circuit breaker, retry mechanisms, fallback strategies +3. **Monitoring & Observability** - Health checks, metrics, distributed tracing +4. **Caching Strategy** - Comprehensive caching with proper invalidation + +### **Performance Targets :** + +1. **API Response Time** - < 200ms (90th percentile) +2. **Search Performance** - < 500ms +3. **File Upload** - < 30 seconds for 50MB files +4. **Cache Hit Ratio** - > 80% + +### **Operational Excellence:** + +1. **Disaster Recovery** - RTO < 4 hours, RPO < 1 hour +2. **Backup Procedures** - Comprehensive backup and restoration +3. **Security Testing** - Penetration testing and security audits +4. **Performance Testing** - Load testing with realistic workloads + +เอกสารนี้สะท้อนถึงความมุ่งมั่นในการสร้างระบบที่มีความปลอดภัย, มีความทนทาน, และมีประสิทธิภาพสูง พร้อมรองรับการเติบโตในอนาคตและความต้องการทางธุรกิจที่เปลี่ยนแปลงไป + +**หมายเหตุ:** แนวทางนี้จะถูกทบทวนและปรับปรุงเป็นระยะตาม feedback จากทีมพัฒนาและความต้องการทางธุรกิจที่เปลี่ยนแปลงไป + +## **Document Control:** + +- **Document:** FullStackJS v1.5.1 +- **Version:** 1.5.1 +- **Date:** 2025-12-04 +- **Author:** NAP LCBP3-DMS & Gemini +- **Status:** FINAL GUIDELINE Rev.06 +- **Classification:** Internal Technical Documentation +- **Approved By:** Nattanin + +--- + +`End of FullStackJS Guidelines v1.5.0` diff --git a/docs/2_Backend_Plan_V1_5_1.md b/docs/2_Backend_Plan_V1_5_1.md new file mode 100644 index 0000000..03adf58 --- /dev/null +++ b/docs/2_Backend_Plan_V1_5_1.md @@ -0,0 +1,210 @@ +# 📋 **แผนการพัฒนา Backend (NestJS) - LCBP3-DMS v1.5.1** + +**สถานะ:** DRAFT +**วันที่:** 2025-12-04 +**อ้างอิง:** Requirements v1.5.1 & FullStackJS Guidelines v1.5.1 +**Classification:** Internal Technical Documentation + +--- + +## 🎯 **ภาพรวมโครงการ (Project Overview)** + +พัฒนา Backend สำหรับระบบบริหารจัดการเอกสารโครงการ (Document Management System) เวอร์ชัน 1.5.1 โดยเน้นการปรับปรุงสถาปัตยกรรมหลัก 3 ส่วนสำคัญ: +1. **Unified Workflow Engine:** ระบบ Workflow แบบ Dynamic ที่ยืดหยุ่น รองรับการกำหนด Rule ผ่าน DSL +2. **Advanced Document Numbering:** ระบบสร้างเลขที่เอกสารที่ซับซ้อน (8-component key) พร้อม Double-Lock Mechanism ป้องกัน Race Condition +3. **Enhanced Master Data:** การจัดการข้อมูลหลักที่ครอบคลุม (Discipline, SubType) และ JSON Schema Management + +--- + +## 📐 **สถาปัตยกรรมระบบ (System Architecture)** + +### **Technology Stack** + +- **Framework:** NestJS (TypeScript, ESM) +- **Database:** MariaDB 10.11 (ใช้ Virtual Columns & Partitioning) +- **ORM:** TypeORM (Optimistic Locking) +- **Workflow Engine:** Custom DSL-based Engine (State Machine) +- **Queue:** BullMQ (Redis) สำหรับ Async Jobs & Notifications +- **Locking:** Redis (Redlock) + DB Pessimistic Fallback +- **Search:** Elasticsearch +- **Validation:** Zod / Class-validator / AJV (JSON Schema) + +### **โครงสร้างโมดูล (Module Structure)** + +``` +📁src +├── 📁common # Shared utilities, guards, decorators +├── 📁config # Configuration setup +├── 📁database # Migrations & Seeds +├── 📁modules +│ ├── 📁auth # Authentication (JWT) +│ ├── 📁user # User & RBAC Management +│ ├── 📁master-data # Organization, Project, Type, Discipline (NEW) +│ ├── 📁document-numbering # Numbering Service (Updated) +│ ├── 📁workflow-engine # Unified Workflow Engine (NEW) +│ ├── 📁correspondence # Correspondence Management +│ ├── 📁rfa # RFA Management +│ ├── 📁drawing # Drawing Management +│ ├── 📁transmittal # Transmittal Management +│ ├── 📁circulation # Circulation Management +│ ├── 📁file-storage # File Upload & Handling +│ ├── 📁json-schema # JSON Schema Registry (NEW) +│ ├── 📁search # Elasticsearch Integration +│ ├── 📁notification # Notification System +│ └── 📁monitoring # Health & Metrics +└── main.ts +``` + +--- + +## 🗓️ **แผนการพัฒนาแบบ Phase-Based** + +### **Phase 1: Core Foundation & Master Data (Week 1-2)** + +**Goal:** เตรียมโครงสร้างพื้นฐานและข้อมูลหลักให้พร้อมสำหรับโมดูลอื่น + +#### **[ ] T1.1 Master Data Module (Enhanced)** +- **Objective:** จัดการข้อมูลหลักทั้งหมดรวมถึงตารางใหม่ใน v1.5.1 +- **Tasks:** + - [ ] Implement `OrganizationService` (CRUD) + - [ ] Implement `ProjectService` & `ContractService` + - [ ] Implement `TypeService` (Correspondence, RFA, Drawing) + - [ ] **[NEW]** Implement `DisciplineService` (CRUD for `disciplines` table) + - [ ] **[NEW]** Implement `CorrespondenceSubTypeService` + - [ ] **[NEW]** Implement `CodeService` (RFA Approve Codes, Status Codes) +- **Deliverables:** API สำหรับจัดการ Master Data ทั้งหมด + +#### **[ ] T1.2 User & Auth Module** +- **Objective:** ระบบผู้ใช้งานและสิทธิ์ (RBAC) +- **Tasks:** + - [ ] Implement `AuthService` (Login, Refresh Token) + - [ ] Implement `UserService` & `UserPreferenceService` + - [ ] Implement RBAC Guards (Global, Org, Project, Contract scopes) +- **Deliverables:** Secure Authentication & Authorization + +--- + +### **Phase 2: Document Numbering & File Storage (Week 3)** + +**Goal:** ระบบเลขที่เอกสารที่ถูกต้องแม่นยำและระบบไฟล์ที่ปลอดภัย + +#### **[ ] T2.1 Document Numbering Module (Major Update)** +- **Objective:** ระบบสร้างเลขที่เอกสารแบบ 8-component key พร้อม Double-Lock +- **Tasks:** + - [ ] Update `DocumentNumberCounter` entity (8-column PK) + - [ ] Implement `DocumentNumberingService` with **Redlock** + - [ ] Implement **DB Optimistic Lock** fallback strategy + - [ ] Implement Token Parser (`{DISCIPLINE}`, `{SUB_TYPE}`, `{RFA_TYPE}`) + - [ ] Create `DocumentNumberAudit` & `DocumentNumberError` tables +- **Deliverables:** Race-condition free numbering system + +#### **[ ] T2.2 File Storage Service** +- **Objective:** Two-Phase Storage Strategy +- **Tasks:** + - [ ] Implement `Upload` (Phase 1: Temp storage) + - [ ] Implement `Commit` (Phase 2: Move to permanent) + - [ ] Integrate **ClamAV** for virus scanning + - [ ] Implement Cleanup Job for orphan files +- **Deliverables:** Secure file upload system + +--- + +### **Phase 3: Unified Workflow Engine (Week 4-5)** + +**Goal:** ระบบ Workflow กลางที่ยืดหยุ่นและ Configurable + +#### **[ ] T3.1 Workflow Engine Core** +- **Objective:** สร้าง Engine สำหรับรัน Workflow ตาม DSL +- **Tasks:** + - [ ] Design DSL Schema (JSON) + - [ ] Implement `DslParserService` & Validator + - [ ] Implement `WorkflowEngineService` (State Machine) + - [ ] Implement `GuardExecutor` (Permission/Condition checks) + - [ ] Implement `EffectExecutor` (Actions after transition) +- **Deliverables:** Functional Workflow Engine + +#### **[ ] T3.2 Workflow Integration** +- **Objective:** เชื่อมต่อ Engine เข้ากับ Business Modules +- **Tasks:** + - [ ] Create Standard Workflow Definitions (Correspondence, RFA) + - [ ] Implement `WorkflowInstance` creation logic + - [ ] Create API for Workflow Actions (Approve, Reject, Comment) +- **Deliverables:** Integrated Workflow System + +--- + +### **Phase 4: Business Logic Modules (Week 6-7)** + +**Goal:** ฟังก์ชันการทำงานหลักของระบบเอกสาร + +#### **[ ] T4.1 Correspondence Module** +- **Objective:** จัดการหนังสือโต้ตอบ +- **Tasks:** + - [ ] Update Entity to support `discipline_id` + - [ ] Integrate with **Document Numbering** + - [ ] Integrate with **Workflow Engine** + - [ ] Implement CRUD & Revision handling + +#### **[ ] T4.2 RFA Module** +- **Objective:** จัดการเอกสารขออนุมัติ +- **Tasks:** + - [ ] Update Entity to support `discipline_id` + - [ ] Implement RFA-specific workflow logic + - [ ] Implement RFA Item linking (Drawings) + +#### **[ ] T4.3 Drawing Module** +- **Objective:** จัดการแบบก่อสร้าง (Shop Drawing, Contract Drawing) +- **Tasks:** + - [ ] Implement `ShopDrawingService` & `ContractDrawingService` + - [ ] Implement Revision Control for Drawings + - [ ] Implement Drawing Numbering Logic + +#### **[ ] T4.4 Transmittal Module** +- **Objective:** จัดการใบนำส่งเอกสาร (Transmittal) +- **Tasks:** + - [ ] Implement `TransmittalService` (Create, View, PDF) + - [ ] Implement `TransmittalItem` linking (Correspondence, RFA, Drawing) + - [ ] Implement Transmittal Numbering (Type 901) + - [ ] Generate PDF Transmittal Letter + +#### **[ ] T4.5 Circulation Module** +- **Objective:** จัดการใบเวียนภายใน (Circulation Sheet) +- **Tasks:** + - [ ] Implement `CirculationService` (Create, Assign, Complete) + - [ ] Implement `CirculationAssignee` tracking (Multiple users) + - [ ] Implement Circulation Numbering (Type 900) + - [ ] Integrate with Workflow for completion tracking + +--- + +### **Phase 5: System, Search & Monitoring (Week 8)** + +**Goal:** ระบบสนับสนุนและการตรวจสอบ + +#### **[ ] T5.1 JSON Schema & Preferences** +- **Objective:** จัดการ Dynamic Data และ User Settings +- **Tasks:** + - [ ] Implement `JsonSchemaService` (Registry & Validation) + - [ ] Implement `UserPreferenceService` + - [ ] Implement Virtual Column management + +#### **[ ] T5.2 Search & Logs** +- **Objective:** การค้นหาและตรวจสอบ +- **Tasks:** + - [ ] Implement **Elasticsearch** Sync + - [ ] Implement **Audit Log** with Partitioning + - [ ] Setup **Prometheus/Grafana** metrics + +--- + +## 🛡️ **Security & Performance Guidelines** + +1. **Double-Locking:** ใช้ Redis Lock คู่กับ DB Optimistic Lock เสมอสำหรับ Critical Sections +2. **Input Validation:** ใช้ Zod/DTO Validation ทุกจุด โดยเฉพาะ JSON Fields +3. **Rate Limiting:** บังคับใช้ Rate Limit ตาม User Role +4. **Audit Logging:** บันทึกทุกการกระทำที่สำคัญลง `audit_logs` +5. **Partitioning:** ใช้ Partitioning สำหรับตารางขนาดใหญ่ (`audit_logs`, `notifications`) + +--- + +**End of Backend Plan V1.5.1** diff --git a/docs/3_Frontend_Plan_V1_5_1.md b/docs/3_Frontend_Plan_V1_5_1.md new file mode 100644 index 0000000..2602afd --- /dev/null +++ b/docs/3_Frontend_Plan_V1_5_1.md @@ -0,0 +1,182 @@ +# 📋 **แผนการพัฒนา Frontend (Next.js) - LCBP3-DMS v1.5.1** + +**สถานะ:** DRAFT +**วันที่:** 2025-12-04 +**อ้างอิง:** Requirements v1.5.1 & FullStackJS Guidelines v1.5.1 +**Classification:** Internal Technical Documentation + +--- + +## 🎯 **ภาพรวมโครงการ (Project Overview)** + +พัฒนา Frontend สำหรับระบบบริหารจัดการเอกสารโครงการ (Document Management System) เวอร์ชัน 1.5.1 โดยเน้นการรองรับฟีเจอร์ใหม่ใน Backend v1.5.1 ได้แก่ **Unified Workflow Engine**, **Advanced Document Numbering**, และ **Enhanced Master Data Management** พร้อมทั้งปรับปรุง UX/UI ให้ทันสมัยและใช้งานง่าย + +--- + +## 📐 **สถาปัตยกรรมระบบ (System Architecture)** + +### **Technology Stack** + +- **Framework:** Next.js 14+ (App Router, React 18, TypeScript) +- **Styling:** Tailwind CSS + Shadcn/UI +- **State Management:** + - **Server:** TanStack Query (React Query) + - **Client:** Zustand + - **Form:** React Hook Form + Zod +- **Workflow Visualization:** ReactFlow (สำหรับ Workflow Builder) +- **Editor:** Monaco Editor (สำหรับ DSL Editing) +- **Validation:** Zod + AJV (JSON Schema) + +### **โครงสร้างโมดูล (Module Structure)** + +``` +📁src +├── 📁app +│ ├── 📁(auth) # Login, Forgot Password +│ ├── 📁(dashboard) # Main App Layout +│ │ ├── 📁admin # Admin Panel (Users, Master Data, Config) +│ │ ├── 📁correspondences +│ │ ├── 📁rfas +│ │ ├── 📁drawings +│ │ ├── 📁transmittals +│ │ ├── 📁circulations +│ │ └── 📁tasks +│ └── 📁api # NextAuth & Proxy +├── 📁components +│ ├── 📁admin # Admin-specific components +│ ├── 📁workflow # Workflow Builder & Visualizer +│ ├── 📁numbering # Numbering Template Editor +│ └── 📁ui # Shadcn UI Components +└── 📁lib + ├── 📁api # API Clients + └── 📁stores # Zustand Stores +``` + +--- + +## 🗓️ **แผนการพัฒนาแบบ Phase-Based** + +### **Phase 1: Foundation & UI Components (Week 1)** + +**Goal:** เตรียมโครงสร้างโปรเจกต์และ Component พื้นฐาน + +#### **[ ] F1.1 Project Setup & Design System** +- **Tasks:** + - [ ] Setup Next.js 14 + Tailwind + Shadcn/UI + - [ ] Configure Axios with **Idempotency Interceptor** + - [ ] Implement Base Layout (Sidebar, Navbar, Breadcrumbs) + - [ ] Setup **TanStack Query** & **Zustand** + +#### **[ ] F1.2 Authentication UI** +- **Tasks:** + - [ ] Login Page with Form Validation + - [ ] Integrate NextAuth.js with Backend + - [ ] Implement RBAC Guard (Protect Routes based on Permissions) + +--- + +### **Phase 2: Admin & Master Data (Week 2-3)** + +**Goal:** ระบบจัดการผู้ใช้และข้อมูลหลัก (รองรับ v1.5.1 Requirements) + +#### **[ ] F2.1 User & Role Management** +- **Tasks:** + - [ ] User List & CRUD (Admin only) + - [ ] Role Assignment UI + - [ ] Permission Matrix Viewer + +#### **[ ] F2.2 Enhanced Master Data UI** +- **Tasks:** + - [ ] **Organization Management:** CRUD + Logo Upload + - [ ] **Project & Contract Management:** CRUD + Relations + - [ ] **[NEW] Discipline Management:** CRUD for `disciplines` + - [ ] **[NEW] Sub-Type Management:** CRUD for `correspondence_sub_types` + +#### **[ ] F2.3 System Configuration** +- **Tasks:** + - [ ] **[NEW] Document Numbering Config:** + - Template Editor (Monaco/Visual) + - Sequence Viewer + - [ ] **[NEW] Workflow Configuration:** + - Workflow List + - DSL Editor (Monaco) + - Visual Builder (ReactFlow) + +--- + +### **Phase 3: Core Modules (Week 4-5)** + +**Goal:** โมดูลหลัก Correspondence และ RFA + +#### **[ ] F3.1 Correspondence Module** +- **Tasks:** + - [ ] List View with Advanced Filters + - [ ] **Create/Edit Form:** + - Add **Discipline Selector** (Dynamic based on Contract) + - Add **Sub-Type Selector** (Dynamic based on Type) + - File Upload (Two-Phase) + - [ ] Detail View with History & Comments + +#### **[ ] F3.2 RFA Module** +- **Tasks:** + - [ ] RFA List & Dashboard + - [ ] **Dynamic RFA Form:** + - Fields change based on RFA Type (DWG, MAT, MES) + - Item List Management + - [ ] **Approval Interface:** + - Approve/Reject/Comment Actions + - Workflow Status Visualization + +--- + +### **Phase 4: Advanced Modules (Week 6-7)** + +**Goal:** โมดูล Drawing, Transmittal และ Circulation + +#### **[ ] F4.1 Drawing Module** +- **Tasks:** + - [ ] Shop Drawing & Contract Drawing Lists + - [ ] Revision Management UI + - [ ] Drawing Viewer (PDF/Image) + +#### **[ ] F4.2 Transmittal Module** +- **Tasks:** + - [ ] Transmittal Creation Form (Select Documents to send) + - [ ] Transmittal Letter Preview (PDF Generation) + - [ ] Transmittal History + +#### **[ ] F4.3 Circulation Module** +- **Tasks:** + - [ ] Circulation Sheet Creation (Select Assignees) + - [ ] "My Tasks" Dashboard for Circulation + - [ ] Completion & Tracking UI + +--- + +### **Phase 5: Search & Dashboard (Week 8)** + +**Goal:** การค้นหาและหน้า Dashboard + +#### **[ ] F5.1 Advanced Search** +- **Tasks:** + - [ ] Unified Search Interface (Elasticsearch) + - [ ] Faceted Filters (Type, Date, Project, Status) + +#### **[ ] F5.2 Dashboard & Monitoring** +- **Tasks:** + - [ ] Personal Dashboard (My Tasks, Pending Approvals) + - [ ] Project Dashboard (KPIs, Stats) + - [ ] **Admin Audit Logs Viewer** + +--- + +## 🛡️ **Security & Performance Guidelines** + +1. **Client-Side Validation:** ใช้ Zod Validate Form ก่อนส่งเสมอ +2. **Optimistic Updates:** ใช้ React Query `onMutate` เพื่อความลื่นไหล +3. **Code Splitting:** ใช้ `React.lazy` และ `Next.js Dynamic Imports` สำหรับ Component ใหญ่ๆ (เช่น Monaco Editor, ReactFlow) +4. **Secure Storage:** ห้ามเก็บ Token ใน LocalStorage (ใช้ HttpOnly Cookie ผ่าน NextAuth) + +--- + +**End of Frontend Plan V1.5.1** diff --git a/docs/4_Data_Dictionary_V1_5_1.md b/docs/4_Data_Dictionary_V1_5_1.md new file mode 100644 index 0000000..03d7820 --- /dev/null +++ b/docs/4_Data_Dictionary_V1_5_1.md @@ -0,0 +1,1866 @@ +# **ตารางฐานข้อมูล (Data Dictionary) - LCBP3-DMS (V1.5.1)** + +เอกสารนี้สรุปโครงสร้างตาราง, Foreign Keys (FK), และ Constraints ที่สำคัญทั้งหมดในฐานข้อมูล LCBP3-DMS (v1.5.1) เพื่อใช้เป็นเอกสารอ้างอิงสำหรับทีมพัฒนา Backend (NestJS) และ Frontend (Next.js) โดยอิงจาก Requirements และ SQL Script ล่าสุด + +**สถานะ:** FINAL GUIDELINE +**วันที่:** 2025-12-04 +**อ้างอิง:** Requirements v1.5.1 & FullStackJS Guidelines v1.5.1 +**Classification:** Internal Technical Documentation + +## 📝 สรุปรายการปรับปรุง (Summary of Changes in v1.5.1) + +1. **Enhanced Document Numbering**: ปรับปรุงตาราง `document_number_counters` ให้รองรับ 8-column Composite PK และเพิ่มตาราง `document_number_audit`, `document_number_errors` +2. **Unified Workflow Engine**: เพิ่มตาราง `workflow_definitions`, `workflow_instances`, `workflow_histories` เพื่อรองรับ Workflow แบบ Dynamic +3. **New Master Tables**: เพิ่มตาราง `disciplines` และ `correspondence_sub_types` ตาม Req 6B +4. **System Enhancements**: เพิ่มตาราง `json_schemas` และ `user_preferences` +5. **Schema Updates**: เพิ่ม `discipline_id` ในตาราง `correspondences` และ `rfas` + +--- + +## **1. 🏢 Core & Master Data Tables (องค์กร, โครงการ, สัญญา)** + +### 1.1 organization_roles + +**Purpose**: Master table for organization role types in the system + +| Column Name | Data Type | Constraints | Description | +| ----------- | ----------- | --------------------------- | ---------------------------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier for organization role | +| role_name | VARCHAR(20) | NOT NULL, UNIQUE | Role name (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD PARTY) | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (role_name) + +**Business Rules**: + +- Predefined system roles for organization types +- Cannot be deleted if referenced by organizations + +--- + +### 1.2 organizations + +**Purpose**: Master table storing all organizations involved in the system + +| Column Name | Data Type | Constraints | Description | +| ----------------- | ------------ | ----------------------------------- | ---------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier for organization | +| organization_code | VARCHAR(20) | NOT NULL, UNIQUE | Organization code (e.g., 'กทท.', 'TEAM') | +| organization_name | VARCHAR(255) | NOT NULL | Full organization name | +| is_active | BOOLEAN | DEFAULT TRUE | Active status (1=active, 0=inactive) | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (organization_code) +- INDEX (is_active) + +**Relationships**: + +- Referenced by: users, project_organizations, contract_organizations, correspondences, circulations + +--- + +### 1.3 projects + +**Purpose**: Master table for all projects in the system + +| Column Name | Data Type | Constraints | Description | +| ------------ | ------------ | --------------------------- | ----------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier for project | +| project_code | VARCHAR(50) | NOT NULL, UNIQUE | Project code (e.g., 'LCBP3') | +| project_name | VARCHAR(255) | NOT NULL | Full project name | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (project_code) +- INDEX (is_active) + +**Relationships**: + +- Referenced by: contracts, correspondences, document_number_formats, drawings + +--- + +### 1.4 contracts + +**Purpose**: Master table for contracts within projects + +| Column Name | Data Type | Constraints | Description | +| ------------- | ------------ | ----------------------------------- | ------------------------------ | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier for contract | +| project_id | INT | NOT NULL, FK | Reference to projects table | +| contract_code | VARCHAR(50) | NOT NULL, UNIQUE | Contract code | +| contract_name | VARCHAR(255) | NOT NULL | Full contract name | +| description | TEXT | NULL | Contract description | +| start_date | DATE | NULL | Contract start date | +| end_date | DATE | NULL | Contract end date | +| is_active | BOOLEAN | DEFAULT TRUE | Active status | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (contract_code) +- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +- INDEX (project_id, is_active) + +**Relationships**: + +- Parent: projects +- Referenced by: contract_organizations, user_assignments + +--- + +### 1.5 disciplines (NEW v1.5.1) + +**Purpose**: เก็บข้อมูลสาขางาน (Disciplines) แยกตามสัญญา (Req 6B) + +| Column Name | Data Type | Constraints | Description | +| :-------------- | :----------- | :----------- | :--------------------- | +| id | INT | PK, AI | Unique identifier | +| contract_id | INT | FK, NOT NULL | ผูกกับสัญญา | +| discipline_code | VARCHAR(10) | NOT NULL | รหัสสาขา (เช่น GEN, STR) | +| code_name_th | VARCHAR(255) | NULL | ชื่อไทย | +| code_name_en | VARCHAR(255) | NULL | ชื่ออังกฤษ | +| is_active | TINYINT(1) | DEFAULT 1 | สถานะการใช้งาน | + +**Indexes**: +- UNIQUE (contract_id, discipline_code) + +--- + +## **2. 👥 Users & RBAC Tables (ผู้ใช้, สิทธิ์, บทบาท)** + +### 2.1 users + +**Purpose**: Master table storing all system users + +| Column Name | Data Type | Constraints | Description | +| ----------------------- | ------------ | ----------------------------------- | -------------------------------- | +| user_id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier for user | +| username | VARCHAR(50) | NOT NULL, UNIQUE | Login username | +| password_hash | VARCHAR(255) | NOT NULL | Hashed password (bcrypt) | +| first_name | VARCHAR(50) | NULL | User's first name | +| last_name | VARCHAR(50) | NULL | User's last name | +| email | VARCHAR(100) | NOT NULL, UNIQUE | Email address | +| line_id | VARCHAR(100) | NULL | LINE messenger ID | +| primary_organization_id | INT | NULL, FK | Primary organization affiliation | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | +| failed_attempts | INT | DEFAULT 0 | Failed login attempts counter | +| locked_until | DATETIME | NULL | Account lock expiration time | +| last_login_at | TIMESTAMP | NULL | Last successful login timestamp | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| deleted_at | DATETIME | NULL | Deleted at | + +**Indexes**: + +- PRIMARY KEY (user_id) +- UNIQUE (username) +- UNIQUE (email) +- FOREIGN KEY (primary_organization_id) REFERENCES organizations(id) ON DELETE SET NULL +- INDEX (is_active) +- INDEX (email) + +**Relationships**: + +- Parent: organizations (primary_organization_id) +- Referenced by: user_assignments, audit_logs, notifications, circulation_routings + +--- + +### 2.2 roles + +**Purpose**: Master table defining system roles with scope levels + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | --------------------------- | ---------------------------------------------------- | +| role_id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier for role | +| role_name | VARCHAR(100) | NOT NULL | Role name (e.g., 'Superadmin', 'Document Control') | +| scope | ENUM | NOT NULL | Scope level: Global, Organization, Project, Contract | +| description | TEXT | NULL | Role description | +| is_system | BOOLEAN | DEFAULT FALSE | System role flag (cannot be deleted) | + +**Indexes**: + +- PRIMARY KEY (role_id) +- INDEX (scope) + +**Relationships**: + +- Referenced by: role_permissions, user_assignments + +--- + +### 2.3 permissions + +**Purpose**: Master table defining all system permissions + +| Column Name | Data Type | Constraints | Description | +| --------------- | ------------ | --------------------------- | ------------------------------------------------------ | +| permission_id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier for permission | +| permission_name | VARCHAR(100) | NOT NULL, UNIQUE | Permission code (e.g., 'rfas.create', 'document.view') | +| description | TEXT | NULL | Permission description | +| module | VARCHAR(50) | NULL | Related module name | +| scope_level | ENUM | NULL | Scope: GLOBAL, ORG, PROJECT | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +- PRIMARY KEY (permission_id) +- UNIQUE (permission_name) +- INDEX (module) +- INDEX (scope_level) +- INDEX (is_active) + +**Relationships**: + +- Referenced by: role_permissions + +--- + +### 2.4 role_permissions + +**Purpose**: Junction table mapping roles to permissions (M:N) + +| Column Name | Data Type | Constraints | Description | +| ------------- | --------- | --------------- | ------------------------------ | +| role_id | INT | PRIMARY KEY, FK | Reference to roles table | +| permission_id | INT | PRIMARY KEY, FK | Reference to permissions table | + +**Indexes**: + +- PRIMARY KEY (role_id, permission_id) +- FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE +- FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ON DELETE CASCADE +- INDEX (permission_id) + +**Relationships**: + +- Parent: roles, permissions + +--- + +### 2.5 user_assignments + +**Purpose**: Junction table assigning users to roles with scope context + +| Column Name | Data Type | Constraints | Description | +| ------------------- | --------- | --------------------------- | ---------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | +| user_id | INT | NOT NULL, FK | Reference to users table | +| role_id | INT | NOT NULL, FK | Reference to roles table | +| organization_id | INT | NULL, FK | Organization scope (if applicable) | +| project_id | INT | NULL, FK | Project scope (if applicable) | +| contract_id | INT | NULL, FK | Contract scope (if applicable) | +| assigned_by_user_id | INT | NULL, FK | User who made the assignment | +| assigned_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Assignment timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +- FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE +- FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE +- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +- FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE +- FOREIGN KEY (assigned_by_user_id) REFERENCES users(user_id) +- INDEX (user_id, role_id) +- INDEX (organization_id) +- INDEX (project_id) +- INDEX (contract_id) + +**Relationships**: + +- Parent: users, roles, organizations, projects, contracts + +--- + +### 2.6 project_organizations + +**Purpose**: Junction table linking projects to participating organizations (M:N) + +| Column Name | Data Type | Constraints | Description | +| --------------- | --------- | --------------- | -------------------------------- | +| project_id | INT | PRIMARY KEY, FK | Reference to projects table | +| organization_id | INT | PRIMARY KEY, FK | Reference to organizations table | + +**Indexes**: + +- PRIMARY KEY (project_id, organization_id) +- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +- FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE +- INDEX (organization_id) + +**Relationships**: + +- Parent: projects, organizations + +--- + +### 2.7 contract_organizations + +**Purpose**: Junction table linking contracts to participating organizations with roles (M:N) + +| Column Name | Data Type | Constraints | Description | +| ---------------- | ------------ | --------------- | ------------------------------------------------------------------------- | +| contract_id | INT | PRIMARY KEY, FK | Reference to contracts table | +| organization_id | INT | PRIMARY KEY, FK | Reference to organizations table | +| role_in_contract | VARCHAR(100) | NULL | Organization's role in contract (Owner, Designer, Consultant, Contractor) | + +**Indexes**: + +- PRIMARY KEY (contract_id, organization_id) +- FOREIGN KEY (contract_id) REFERENCES contracts(id) ON DELETE CASCADE +- FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE +- INDEX (organization_id) +- INDEX (role_in_contract) + +**Relationships**: + +- Parent: contracts, organizations + +--- + +### 2.8 user_preferences (NEW v1.5.1) + +**Purpose**: เก็บการตั้งค่าส่วนตัวของผู้ใช้ (Req 5.5, 6.8.3) + +| Column Name | Data Type | Constraints | Description | +| :----------- | :---------- | :-------------- | :-------------- | +| user_id | INT | PK, FK | User ID | +| notify_email | BOOLEAN | DEFAULT TRUE | รับอีเมลแจ้งเตือน | +| notify_line | BOOLEAN | DEFAULT TRUE | รับไลน์แจ้งเตือน | +| digest_mode | BOOLEAN | DEFAULT FALSE | รับแจ้งเตือนแบบรวม | +| ui_theme | VARCHAR(20) | DEFAULT 'light' | UI Theme | + +--- + +## **3. ✉️ Correspondences Tables (เอกสารหลัก, Revisions, Workflows)** + +### 3.1 correspondence_types + +**Purpose**: Master table for correspondence document types + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | --------------------------- | --------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | +| type_code | VARCHAR(50) | NOT NULL, UNIQUE | Type code (e.g., 'RFA', 'RFI', 'TRANSMITTAL') | +| type_name | VARCHAR(255) | NOT NULL | Full type name | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (type_code) +- INDEX (is_active) +- INDEX (sort_order) + +**Relationships**: + +- Referenced by: correspondences, document_number_formats, document_number_counters + +--- + +### 3.2 correspondence_sub_types (NEW v1.5.1) + +**Purpose**: เก็บประเภทหนังสือย่อย (Sub Types) สำหรับ Mapping เลขรหัส (Req 6B) + +| Column Name | Data Type | Constraints | Description | +| :--------------------- | :----------- | :----------- | :------------------------ | +| id | INT | PK, AI | Unique identifier | +| contract_id | INT | FK, NOT NULL | ผูกกับสัญญา | +| correspondence_type_id | INT | FK, NOT NULL | ผูกกับประเภทเอกสารหลัก | +| sub_type_code | VARCHAR(20) | NOT NULL | รหัสย่อย (เช่น MAT, SHP) | +| sub_type_name | VARCHAR(255) | NULL | ชื่อประเภทหนังสือย่อย | +| sub_type_number | VARCHAR(10) | NULL | เลขรหัสสำหรับ Running Number | + +--- + +### 3.3 correspondences (UPDATE v1.5.1) + +**Purpose**: Master table for correspondence documents (non-revisioned data) + +| Column Name | Data Type | Constraints | Description | +| ------------------------- | ------------ | --------------------------- | ------------------------------------------ | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Master correspondence ID | +| correspondence_number | VARCHAR(100) | NOT NULL | Document number (from numbering system) | +| correspondence_type_id | INT | NOT NULL, FK | Reference to correspondence_types | +| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** | +| is_internal_communication | TINYINT(1) | DEFAULT 0 | Internal (1) or external (0) communication | +| project_id | INT | NOT NULL, FK | Reference to projects table | +| originator_id | INT | NULL, FK | Originating organization | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| created_by | INT | NULL, FK | User who created the record | +| deleted_at | DATETIME | NULL | Soft delete timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE RESTRICT +- **FOREIGN KEY (discipline_id) REFERENCES disciplines(id) ON DELETE SET NULL** +- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +- FOREIGN KEY (originator_id) REFERENCES organizations(id) ON DELETE SET NULL +- FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL +- UNIQUE KEY (project_id, correspondence_number) +- INDEX (correspondence_type_id) +- INDEX (originator_id) +- INDEX (deleted_at) + +**Relationships**: + +- Parent: correspondence_types, **disciplines**, projects, organizations, users +- Children: correspondence_revisions, correspondence_recipients, correspondence_tags, correspondence_references, correspondence_attachments, circulations, transmittals + +--- + +### 3.4 correspondence_revisions (UPDATE v1.5.1) + +**Purpose**: Child table storing revision history of correspondences (1:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------------ | ------------ | --------------------------------- | -------------------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | +| correspondence_id | INT | NOT NULL, FK | Master correspondence 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 | +| correspondence_status_id | INT | NOT NULL, FK | Current status of this revision | +| title | VARCHAR(255) | NOT NULL | Document title | +| document_date | DATE | NULL | Document date | +| issued_date | DATETIME | NULL | Issue date | +| received_date | DATETIME | NULL | Received date | +| due_date | DATETIME | NULL | Due date for response | +| description | TEXT | NULL | Revision description | +| details | JSON | NULL | Type-specific details (e.g., RFI questions) | +| 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 | +| v_ref_project_id | INT | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Project ID จาก JSON details เพื่อทำ Index | +| v_ref_type | VARCHAR(50) | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Type จาก JSON details | +| v_doc_subtype | VARCHAR(50) | GENERATED ALWAYS AS (...) VIRTUAL | Virtual Column ดึง Type จาก JSON details | +| schema_version | INT | DEFAULT 1 | Version of the schema used with this details | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +- FOREIGN KEY (correspondence_status_id) REFERENCES correspondence_status(id) ON DELETE RESTRICT +- 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 (correspondence_id, revision_number) +- UNIQUE KEY (correspondence_id, is_current) +- INDEX (correspondence_status_id) +- INDEX (is_current) +- INDEX (document_date) +- INDEX (issued_date) +- INDEX (v_ref_project_id) +- INDEX (v_ref_type) +- INDEX (v_doc_subtype) + +--- + +### 3.5 correspondence_recipients + +**Purpose**: Junction table for correspondence recipients (TO/CC) (M:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------------- | ---------------- | --------------- | ---------------------------- | +| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences | +| recipient_organization_id | INT | PRIMARY KEY, FK | Recipient organization | +| recipient_type | ENUM('TO', 'CC') | PRIMARY KEY | Recipient type | + +**Indexes**: + +- PRIMARY KEY (correspondence_id, recipient_organization_id, recipient_type) +- FOREIGN KEY (correspondence_id) REFERENCES correspondence_revisions(correspondence_id) ON DELETE CASCADE +- FOREIGN KEY (recipient_organization_id) REFERENCES organizations(id) ON DELETE RESTRICT +- INDEX (recipient_organization_id) +- INDEX (recipient_type) + +**Relationships**: + +- Parent: correspondences, organizations + +--- + +### 3.6 tags + +**Purpose**: Master table for document tagging system + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | ----------------------------------- | ------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique tag ID | +| tag_name | VARCHAR(100) | NOT NULL, UNIQUE | Tag name | +| description | TEXT | NULL | Tag description | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (tag_name) +- INDEX (tag_name) - For autocomplete + +**Relationships**: + +- Referenced by: correspondence_tags + +--- + +### 3.7 correspondence_tags + +**Purpose**: Junction table linking correspondences to tags (M:N) + +| Column Name | Data Type | Constraints | Description | +| ----------------- | --------- | --------------- | ---------------------------- | +| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences | +| tag_id | INT | PRIMARY KEY, FK | Reference to tags | + +**Indexes**: + +- PRIMARY KEY (correspondence_id, tag_id) +- FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +- FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +- INDEX (tag_id) + +**Relationships**: + +- Parent: correspondences, tags + +--- + +### 3.8 correspondence_references + +**Purpose**: Junction table for cross-referencing correspondences (M:N) + +| Column Name | Data Type | Constraints | Description | +| --------------------- | --------- | --------------- | ------------------------------------- | +| src_correspondence_id | INT | PRIMARY KEY, FK | Source correspondence ID | +| tgt_correspondence_id | INT | PRIMARY KEY, FK | Target (referenced) correspondence ID | + +**Indexes**: + +- PRIMARY KEY (src_correspondence_id, tgt_correspondence_id) +- FOREIGN KEY (src_correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +- FOREIGN KEY (tgt_correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +- INDEX (tgt_correspondence_id) + +**Relationships**: + +- Parent: correspondences (both sides) + +--- + +## **4. 📐 approval: RFA Tables (เอกสารขออนุมัติ, Workflows)** + +### 4.1 rfa_types + +**Purpose**: Master table for RFA (Request for Approval) types + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | --------------------------- | ------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | +| type_code | VARCHAR(20) | NOT NULL, UNIQUE | Type code (DWG, DOC, MAT, etc.) | +| type_name | VARCHAR(100) | NOT NULL | Full type name | +| description | TEXT | NULL | Type description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (type_code) +- INDEX (is_active) +- INDEX (sort_order) + +**Relationships**: + +- Referenced by: rfas + +--- + +### 4.2 rfa_status_codes + +**Purpose**: Master table for RFA status codes + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | --------------------------- | --------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | +| status_code | VARCHAR(20) | NOT NULL, UNIQUE | Status code (DFT, FAP, FRE, etc.) | +| status_name | VARCHAR(100) | NOT NULL | Full status name | +| description | TEXT | NULL | Status description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (status_code) +- INDEX (is_active) +- INDEX (sort_order) + +**Relationships**: + +- Referenced by: rfa_revisions + +--- + +### 4.3 rfa_approve_codes + +**Purpose**: Master table for RFA approval result codes + +| Column Name | Data Type | Constraints | Description | +| ------------ | ------------ | --------------------------- | -------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique identifier | +| approve_code | VARCHAR(20) | NOT NULL, UNIQUE | Approval code (1A, 1C, 3R, etc.) | +| approve_name | VARCHAR(100) | NOT NULL | Full approval name | +| description | TEXT | NULL | Code description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (approve_code) +- INDEX (is_active) +- INDEX (sort_order) + +**Relationships**: + +- Referenced by: rfa_revisions + +--- + +### 4.4 rfas (UPDATE v1.5.1) + +**Purpose**: Master table for RFA documents (non-revisioned data) + +| Column Name | Data Type | Constraints | Description | +| ----------------- | --------- | --------------------------- | --------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Master RFA ID | +| rfa_type_id | INT | NOT NULL, FK | Reference to rfa_types | +| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** | +| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| created_by | INT | NULL, FK | User who created the record | +| deleted_at | DATETIME | NULL | Soft delete timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (rfa_type_id) REFERENCES rfa_types(id) +- **FOREIGN KEY (discipline_id) REFERENCES disciplines(id) ON DELETE SET NULL** +- FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL +- INDEX (rfa_type_id) +- INDEX (deleted_at) + +**Relationships**: + +- Parent: rfa_types, **disciplines**, users +- Children: rfa_revisions + +--- + +### 4.5 rfa_revisions (UPDATE v1.5.1) + +**Purpose**: Child table storing revision history of RFAs (1:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------- | ------------ | --------------------------------- | ----------------------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | +| correspondence_id | INT | NOT NULL, FK | Link to correspondence (RFA as correspondence) | +| 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_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) | +| 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 | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +- FOREIGN KEY (rfa_id) REFERENCES rfas(id) ON DELETE CASCADE +- 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 (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_approve_code_id) +- INDEX (is_current) +- INDEX (v_ref_drawing_count): ตัวอย่างการ Index ข้อมูลตัวเลขใน JSON + +**Relationships**: + +- Parent: correspondences, rfas, rfa_status_codes, rfa_approve_codes, users +- Children: rfa_items, rfa_workflows + +--- + +### 4.6 rfa_items + +**Purpose**: Junction table linking RFA revisions to shop drawing revisions (M:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------------ | --------- | --------------- | ------------------------------ | +| rfarev_correspondence_id | INT | PRIMARY KEY, FK | RFA revision correspondence ID | +| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Shop drawing revision ID | + +**Indexes**: + +- PRIMARY KEY (rfarev_correspondence_id, shop_drawing_revision_id) +- FOREIGN KEY (rfarev_correspondence_id) REFERENCES rfa_revisions(correspondence_id) ON DELETE CASCADE +- FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE +- INDEX (shop_drawing_revision_id) + +**Relationships**: + +- Parent: rfa_revisions, shop_drawing_revisions + +**Business Rules**: + +- Used primarily for RFA type = 'DWG' (Shop Drawing) +- One RFA can contain multiple shop drawings +- One shop drawing can be referenced by multiple RFAs + +--- + +### 4.7 rfa_workflows + +**Purpose**: Transaction log table tracking actual RFA approval workflow execution + +| Column Name | Data Type | Constraints | Description | +| --------------- | --------- | ----------------------------------- | ------------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique workflow log ID | +| rfa_revision_id | INT | NOT NULL, FK | Reference to RFA revision | +| step_number | INT | NOT NULL | Current step number | +| organization_id | INT | NOT NULL, FK | Organization responsible | +| assigned_to | INT | NULL, FK | Assigned user ID | +| action_type | ENUM | NULL | Action type: REVIEW, APPROVE, ACKNOWLEDGE | +| status | ENUM | NULL | Status: PENDING, IN_PROGRESS, COMPLETED, REJECTED | +| comments | TEXT | NULL | Comments/remarks | +| completed_at | DATETIME | NULL | Completion timestamp | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| state_context | JSON\* | NULL | เก็บข้อมูล Context ของ Workflow ณ ขณะนั้น (Snapshot) | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (rfa_revision_id) REFERENCES rfa_revisions(id) ON DELETE CASCADE +- FOREIGN KEY (organization_id) REFERENCES organizations(id) +- FOREIGN KEY (assigned_to) REFERENCES users(user_id) +- INDEX (rfa_revision_id, step_number) +- INDEX (assigned_to, status) +- INDEX (status) + +**Relationships**: + +- Parent: rfa_revisions, organizations, users + +**Business Rules**: + +- Records actual workflow execution history +- Tracks who did what and when +- Multiple records per RFA revision (one per step) +- Status changes tracked via updated_at + +--- + +## **5. 📐 Drawings Tables (แบบ, หมวดหมู่)** + +### 5.1 contract_drawing_volumes + +**Purpose**: Master table for contract drawing volume classification + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | ----------------------------------- | ------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique volume ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| volume_code | VARCHAR(50) | NOT NULL | Volume code | +| volume_name | VARCHAR(255) | NOT NULL | Volume name | +| description | TEXT | NULL | Volume description | +| sort_order | INT | DEFAULT 0 | Display order | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +- UNIQUE KEY (project_id, volume_code) +- INDEX (sort_order) + +**Relationships**: + +- Parent: projects +- Referenced by: contract_drawings + +**Business Rules**: + +- Volume codes must be unique within a project +- Used for organizing large sets of contract drawings + +--- + +### 5.2 contract_drawing_cats + +**Purpose**: Master table for contract drawing main categories + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | ----------------------------------- | ------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique category ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| cat_code | VARCHAR(50) | NOT NULL | Category code | +| cat_name | VARCHAR(255) | NOT NULL | Category name | +| description | TEXT | NULL | Category description | +| sort_order | INT | DEFAULT 0 | Display order | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +- UNIQUE KEY (project_id, cat_code) +- INDEX (sort_order) + +**Relationships**: + +- Parent: projects +- Referenced by: contract_drawing_subcat_cat_maps + +**Business Rules**: + +- Category codes must be unique within a project +- Hierarchical relationship with sub-categories via mapping table + +--- + +### 5.3 contract_drawing_sub_cats + +**Purpose**: Master table for contract drawing sub-categories + +| Column Name | Data Type | Constraints | Description | +| ------------ | ------------ | ----------------------------------- | ------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique sub-category ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| sub_cat_code | VARCHAR(50) | NOT NULL | Sub-category code | +| sub_cat_name | VARCHAR(255) | NOT NULL | Sub-category name | +| description | TEXT | NULL | Sub-category description | +| sort_order | INT | DEFAULT 0 | Display order | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +- UNIQUE KEY (project_id, sub_cat_code) +- INDEX (sort_order) + +**Relationships**: + +- Parent: projects +- Referenced by: contract_drawings, contract_drawing_subcat_cat_maps + +**Business Rules**: + +- Sub-category codes must be unique within a project +- Can be mapped to multiple main categories via mapping table + +--- + +### 5.4 contract_drawing_subcat_cat_maps + +**Purpose**: Junction table mapping sub-categories to main categories (M:N) + +| Column Name | Data Type | Constraints | Description | +| ----------- | --------- | --------------- | -------------------------- | +| project_id | INT | PRIMARY KEY, FK | Reference to projects | +| sub_cat_id | INT | PRIMARY KEY, FK | Reference to sub-category | +| cat_id | INT | PRIMARY KEY, FK | Reference to main category | + +**Indexes**: + +- PRIMARY KEY (project_id, sub_cat_id, cat_id) +- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +- FOREIGN KEY (sub_cat_id) REFERENCES contract_drawing_sub_cats(id) ON DELETE CASCADE +- FOREIGN KEY (cat_id) REFERENCES contract_drawing_cats(id) ON DELETE CASCADE +- INDEX (sub_cat_id) +- INDEX (cat_id) + +**Relationships**: + +- Parent: projects, contract_drawing_sub_cats, contract_drawing_cats + +**Business Rules**: + +- Allows flexible categorization +- One sub-category can belong to multiple main categories +- All three fields required for uniqueness + +--- + +### 5.5 contract_drawings + +**Purpose**: Master table for contract drawings (from contract specifications) + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | ----------------------------------- | ------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| condwg_no | VARCHAR(255) | NOT NULL | Contract drawing number | +| title | VARCHAR(255) | NOT NULL | Drawing title | +| sub_cat_id | INT | NULL, FK | Reference to sub-category | +| volume_id | INT | NULL, FK | Reference to volume | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | +| updated_by | INT | NULL, FK | User who last updated | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +- FOREIGN KEY (sub_cat_id) REFERENCES contract_drawing_sub_cats(id) ON DELETE RESTRICT +- FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes(id) ON DELETE RESTRICT +- FOREIGN KEY (updated_by) REFERENCES users(user_id) +- UNIQUE KEY (project_id, condwg_no) +- INDEX (sub_cat_id) +- INDEX (volume_id) +- INDEX (deleted_at) + +**Relationships**: + +- Parent: projects, contract_drawing_sub_cats, contract_drawing_volumes, users +- Referenced by: shop_drawing_revision_contract_refs, contract_drawing_attachments + +**Business Rules**: + +- Drawing numbers must be unique within a project +- Represents baseline/contract drawings +- Referenced by shop drawings for compliance tracking +- Soft delete preserves history + +--- + +### 5.6 shop_drawing_main_categories + +**Purpose**: Master table for shop drawing main categories (discipline-level) + +| Column Name | Data Type | Constraints | Description | +| ------------------ | ------------ | ----------------------------------- | ------------------------------------ | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique category ID | +| main_category_code | VARCHAR(50) | NOT NULL, UNIQUE | Category code (ARCH, STR, MEP, etc.) | +| main_category_name | VARCHAR(255) | NOT NULL | Category name | +| description | TEXT | NULL | Category description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (main_category_code) +- INDEX (is_active) +- INDEX (sort_order) + +**Relationships**: + +- Referenced by: shop_drawing_sub_categories, shop_drawings + +**Business Rules**: + +- Global categories (not project-specific) +- Typically represents engineering disciplines + +--- + +### 5.7 shop_drawing_sub_categories + +**Purpose**: Master table for shop drawing sub-categories (component-level) + +| Column Name | Data Type | Constraints | Description | +| ----------------- | ------------ | ----------------------------------- | ----------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique sub-category ID | +| sub_category_code | VARCHAR(50) | NOT NULL, UNIQUE | Sub-category code (STR-COLUMN, ARCH-DOOR, etc.) | +| sub_category_name | VARCHAR(255) | NOT NULL | Sub-category name | +| main_category_id | INT | NOT NULL, FK | Reference to main category | +| description | TEXT | NULL | Sub-category description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (sub_category_code) +- FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id) +- INDEX (main_category_id) +- INDEX (is_active) +- INDEX (sort_order) + +**Relationships**: + +- Parent: shop_drawing_main_categories +- Referenced by: shop_drawings + +**Business Rules**: + +- Global sub-categories (not project-specific) +- Hierarchical under main categories +- Represents specific drawing types or components + +--- + +### 5.8 shop_drawings + +**Purpose**: Master table for shop drawings (contractor-submitted) + +| Column Name | Data Type | Constraints | Description | +| ---------------- | ------------ | ----------------------------------- | -------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | Shop drawing number | +| title | VARCHAR(500) | NOT NULL | Drawing title | +| main_category_id | INT | NOT NULL, FK | Reference to main category | +| sub_category_id | INT | NOT NULL, FK | Reference to sub-category | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | +| deleted_at | DATETIME | NULL | Soft delete timestamp | +| updated_by | INT | NULL, FK | User who last updated | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (drawing_number) +- FOREIGN KEY (project_id) REFERENCES projects(id) +- FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id) +- FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id) +- FOREIGN KEY (updated_by) REFERENCES users(user_id) +- INDEX (project_id) +- INDEX (main_category_id) +- INDEX (sub_category_id) +- INDEX (deleted_at) + +**Relationships**: + +- Parent: projects, shop_drawing_main_categories, shop_drawing_sub_categories, users +- Children: shop_drawing_revisions + +**Business Rules**: + +- Drawing numbers are globally unique across all projects +- Represents contractor shop drawings +- Can have multiple revisions +- Soft delete preserves history + +--- + +### 5.9 shop_drawing_revisions + +**Purpose**: Child table storing revision history of shop drawings (1:N) + +| Column Name | Data Type | Constraints | Description | +| --------------- | ----------- | --------------------------- | ------------------------------ | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID | +| shop_drawing_id | INT | NOT NULL, FK | Master shop drawing ID | +| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) | +| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) | +| revision_date | DATE | NULL | Revision date | +| description | TEXT | NULL | Revision description/changes | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Revision creation timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (shop_drawing_id) REFERENCES shop_drawings(id) ON DELETE CASCADE +- UNIQUE KEY (shop_drawing_id, revision_number) +- INDEX (revision_date) + +**Relationships**: + +- Parent: shop_drawings +- Referenced by: rfa_items, shop_drawing_revision_contract_refs, shop_drawing_revision_attachments + +**Business Rules**: + +- Revision numbers are sequential starting from 0 +- Each revision can reference multiple contract drawings +- Each revision can have multiple file attachments +- Linked to RFAs for approval tracking + +--- + +### 5.10 shop_drawing_revision_contract_refs + +**Purpose**: Junction table linking shop drawing revisions to referenced contract drawings (M:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------------ | --------- | --------------- | ---------------------------------- | +| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to shop drawing revision | +| contract_drawing_id | INT | PRIMARY KEY, FK | Reference to contract drawing | + +**Indexes**: + +- PRIMARY KEY (shop_drawing_revision_id, contract_drawing_id) +- FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE +- FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings(id) ON DELETE CASCADE +- INDEX (contract_drawing_id) + +**Relationships**: + +- Parent: shop_drawing_revisions, contract_drawings + +**Business Rules**: + +- Tracks which contract drawings each shop drawing revision is based on +- Ensures compliance with contract specifications +- One shop drawing revision can reference multiple contract drawings + +--- + +## **6. 🔄 Circulations Tables (ใบเวียนภายใน)** + +### 6.1 circulation_status_codes + +**Purpose**: Master table for circulation workflow status codes + +| Column Name | Data Type | Constraints | Description | +| ----------- | ----------- | --------------------------- | --------------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique status ID | +| code | VARCHAR(20) | NOT NULL, UNIQUE | Status code (OPEN, IN_REVIEW, COMPLETED, CANCELLED) | +| description | VARCHAR(50) | NOT NULL | Status description | +| sort_order | INT | DEFAULT 0 | Display order | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (code) +- INDEX (is_active) +- INDEX (sort_order) + +**Relationships**: + +- Referenced by: circulations + +**Seed Data**: 4 status codes + +- OPEN: Initial status when created +- IN_REVIEW: Under review by recipients +- COMPLETED: All recipients have responded +- CANCELLED: Withdrawn/cancelled + +--- + +### 6.2 circulations + +**Purpose**: Master table for internal circulation sheets (document routing) + +| Column Name | Data Type | Constraints | Description | +| ----------------------- | ------------ | ----------------------------------- | ----------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique circulation ID | +| correspondence_id | INT | UNIQUE, FK | Link to correspondence (1:1 relationship) | +| organization_id | INT | NOT NULL, FK | Organization that owns this circulation | +| circulation_no | VARCHAR(100) | NOT NULL | Circulation sheet number | +| circulation_subject | VARCHAR(500) | NOT NULL | Subject/title | +| circulation_status_code | VARCHAR(20) | NOT NULL, FK | Current status code | +| created_by_user_id | INT | NOT NULL, FK | User who created circulation | +| submitted_at | TIMESTAMP | NULL | Submission timestamp | +| closed_at | TIMESTAMP | NULL | Closure timestamp | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | +| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp | + +**Indexes**: + +- PRIMARY KEY (id) +- UNIQUE (correspondence_id) +- FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) +- FOREIGN KEY (organization_id) REFERENCES organizations(id) +- FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes(code) +- FOREIGN KEY (created_by_user_id) REFERENCES users(user_id) +- INDEX (organization_id) +- INDEX (circulation_status_code) +- INDEX (created_by_user_id) + +**Relationships**: + +- Parent: correspondences, organizations, circulation_status_codes, users +- Children: circulation_routings, circulation_attachments + +**Business Rules**: + +- Internal document routing within organization +- One-to-one relationship with correspondences +- Tracks document review/approval workflow +- Status progression: OPEN → IN_REVIEW → COMPLETED/CANCELLED + +--- + +## **7. 📤 Transmittals Tables (เอกสารนำส่ง)** + +### 7.1 transmittals + +**Purpose**: Child table for transmittal-specific data (1:1 with correspondences) + +| Column Name | Data Type | Constraints | Description | +| ----------------- | --------- | --------------- | --------------------------------------------------------- | +| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences (1:1) | +| purpose | ENUM | NULL | Purpose: FOR_APPROVAL, FOR_INFORMATION, FOR_REVIEW, OTHER | +| remarks | TEXT | NULL | Additional remarks | + +**Indexes**: + +- PRIMARY KEY (correspondence_id) +- FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +- INDEX (purpose) + +**Relationships**: + +- Parent: correspondences +- Children: transmittal_items + +**Business Rules**: + +- One-to-one relationship with correspondences +- Transmittal is a correspondence type for forwarding documents +- Contains metadata about the transmission + +--- + +### 7.2 transmittal_items + +**Purpose**: Junction table listing documents included in transmittal (M:N) + +| Column Name | Data Type | Constraints | Description | +| ---------------------- | ------------ | --------------------------- | --------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique item ID | +| transmittal_id | INT | NOT NULL, FK | Reference to transmittal | +| item_correspondence_id | INT | NOT NULL, FK | Reference to document being transmitted | +| quantity | INT | DEFAULT 1 | Number of copies | +| remarks | VARCHAR(255) | NULL | Item-specific remarks | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (transmittal_id) REFERENCES transmittals(correspondence_id) ON DELETE CASCADE +- FOREIGN KEY (item_correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +- UNIQUE KEY (transmittal_id, item_correspondence_id) +- INDEX (item_correspondence_id) + +**Relationships**: + +- Parent: transmittals, correspondences + +**Business Rules**: + +- One transmittal can contain multiple documents +- Tracks quantity of physical copies (if applicable) +- Links to any type of correspondence document + +--- + +## **8. 📎 File Management Tables (ไฟล์แนบ)** + +### 8.1 attachments + +**Purpose**: Central repository for all file attachments in the system + +| Column Name | Data Type | Constraints | Description | +| ------------------- | ------------ | --------------------------- | -------------------------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique attachment ID | +| original_filename | VARCHAR(255) | NOT NULL | Original filename from upload | +| stored_filename | VARCHAR(255) | NOT NULL | System-generated unique filename | +| file_path | VARCHAR(500) | NOT NULL | Full file path on server (/share/dms-data/) | +| mime_type | VARCHAR(100) | NOT NULL | MIME type (application/pdf, image/jpeg, etc.) | +| file_size | INT | NOT NULL | File size in bytes | +| uploaded_by_user_id | INT | NOT NULL, FK | User who uploaded file | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Upload timestamp | +| is_temporary | BOOLEAN | DEFAULT TRUE | ระบุว่าเป็นไฟล์ชั่วคราว (ยังไม่ได้ Commit) | +| temp_id\* | VARCHAR(100) | NULL | ID ชั่วคราวสำหรับอ้างอิงตอน Upload Phase 1 (อาจใช้ร่วมกับ id หรือแยกก็ได้) | +| expires_at | DATETIME | NULL | เวลาหมดอายุของไฟล์ Temp (เพื่อให้ Cron Job ลบออก) | +| checksum | VARCHAR(64) | NULL | SHA-256 Checksum สำหรับ Verify File Integrity [Req 3.9.3] | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (uploaded_by_user_id) REFERENCES users(user_id) ON DELETE CASCADE +- INDEX (stored_filename) +- INDEX (mime_type) +- INDEX (uploaded_by_user_id) +- INDEX (created_at) + +**Relationships**: + +- Parent: users +- Referenced by: correspondence_attachments, circulation_attachments, shop_drawing_revision_attachments, contract_drawing_attachments + +**Business Rules**: + +- Central storage prevents file duplication +- Stored filename prevents naming conflicts +- File path points to QNAP NAS storage +- Original filename preserved for download +- One file record can be linked to multiple documents + +--- + +### 8.2 correspondence_attachments + +**Purpose**: Junction table linking correspondences to file attachments (M:N) + +| Column Name | Data Type | Constraints | Description | +| ----------------- | --------- | --------------- | ---------------------------- | +| correspondence_id | INT | PRIMARY KEY, FK | Reference to correspondences | +| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | +| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | + +**Indexes**: + +- PRIMARY KEY (correspondence_id, attachment_id) +- FOREIGN KEY (correspondence_id) REFERENCES correspondences(id) ON DELETE CASCADE +- FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE +- INDEX (attachment_id) +- INDEX (is_main_document) + +**Relationships**: + +- Parent: correspondences, attachments + +**Business Rules**: + +- One correspondence can have multiple attachments +- One attachment can be linked to multiple correspondences +- is_main_document identifies primary file (typically PDF) + +--- + +### 8.3 circulation_attachments + +**Purpose**: Junction table linking circulations to file attachments (M:N) + +| Column Name | Data Type | Constraints | Description | +| ---------------- | --------- | --------------- | -------------------------- | +| circulation_id | INT | PRIMARY KEY, FK | Reference to circulations | +| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | +| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | + +**Indexes**: + +- PRIMARY KEY (circulation_id, attachment_id) +- FOREIGN KEY (circulation_id) REFERENCES circulations(id) ON DELETE CASCADE +- FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE +- INDEX (attachment_id) +- INDEX (is_main_document) + +**Relationships**: + +- Parent: circulations, attachments + +--- + +### 8.4 shop_drawing_revision_attachments + +**Purpose**: Junction table linking shop drawing revisions to file attachments (M:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------------ | --------- | --------------- | ---------------------------------- | +| shop_drawing_revision_id | INT | PRIMARY KEY, FK | Reference to shop drawing revision | +| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | +| file_type | ENUM | NULL | File type: PDF, DWG, SOURCE, OTHER | +| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | + +**Indexes**: + +- PRIMARY KEY (shop_drawing_revision_id, attachment_id) +- FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions(id) ON DELETE CASCADE +- FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE +- INDEX (attachment_id) +- INDEX (file_type) +- INDEX (is_main_document) + +**Relationships**: + +- Parent: shop_drawing_revisions, attachments + +**Business Rules**: + +- file_type categorizes drawing file formats +- Typically includes PDF for viewing and DWG for editing +- SOURCE may include native CAD files + +--- + +### 8.5 contract_drawing_attachments + +**Purpose**: Junction table linking contract drawings to file attachments (M:N) + +| Column Name | Data Type | Constraints | Description | +| ------------------- | --------- | --------------- | ---------------------------------- | +| contract_drawing_id | INT | PRIMARY KEY, FK | Reference to contract drawing | +| attachment_id | INT | PRIMARY KEY, FK | Reference to attachments | +| file_type | ENUM | NULL | File type: PDF, DWG, SOURCE, OTHER | +| is_main_document | BOOLEAN | DEFAULT FALSE | Main/primary document flag | + +**Indexes**: + +- PRIMARY KEY (contract_drawing_id, attachment_id) +- FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings(id) ON DELETE CASCADE +- FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE +- INDEX (attachment_id) +- INDEX (file_type) +- INDEX (is_main_document) + +**Relationships**: + +- Parent: contract_drawings, attachments + +--- + +## **9. 🔢 Document Numbering System Tables (ระบบเลขที่เอกสาร)** + +### 9.1 document_number_formats + +**Purpose**: Master table defining numbering formats for each document type + +| Column Name | Data Type | Constraints | Description | +| ---------------------- | ------------ | --------------------------- | -------------------------------------------- | +| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique format ID | +| project_id | INT | NOT NULL, FK | Reference to projects | +| correspondence_type_id | INT | NOT NULL, FK | Reference to correspondence_types | +| format_string | VARCHAR(100) | NOT NULL | Format pattern (e.g., {ORG}-{TYPE}-{YYYY}-#) | +| description | TEXT | NULL | Format description | +| is_active | TINYINT(1) | DEFAULT 1 | Active status | + +**Indexes**: + +- PRIMARY KEY (id) +- FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +- FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types(id) ON DELETE CASCADE +- UNIQUE KEY (project_id, correspondence_type_id) +- INDEX (is_active) + +**Relationships**: + +- Parent: projects, correspondence_types + +**Business Rules**: + +- Defines how document numbers are constructed +- Supports placeholders: {PROJ}, {ORG}, {TYPE}, {YYYY}, {MM}, {#} + +--- + +### 9.2 document_number_counters (UPDATE v1.5.1) + +**Purpose**: Transaction table tracking running numbers (High Concurrency) + +| Column Name | Data Type | Constraints | Description | +| -------------------------- | --------- | ------------- | -------------------------------------------- | +| project_id | INT | PK, NOT NULL | โครงการ | +| originator_organization_id | INT | PK, NOT NULL | องค์กรผู้ส่ง | +| recipient_organization_id | INT | PK, NOT NULL | [NEW] องค์กรผู้รับ (-1 = ทุกองค์กร) | +| correspondence_type_id | INT | PK, NOT NULL | ประเภทเอกสาร | +| sub_type_id | INT | PK, DEFAULT 0 | [NEW] ประเภทย่อย สำหรับ TRANSMITTAL (0 = ไม่ระบุ) | +| rfa_type_id | INT | PK, DEFAULT 0 | [NEW] ประเภท RFA (0 = ไม่ใช่ RFA) | +| discipline_id | INT | PK, DEFAULT 0 | [NEW] สาขางาน (0 = ไม่ระบุ) | +| current_year | INT | PK, NOT NULL | ปี ค.ศ. ของตัวนับ (auto-reset ทุกปี) | +| last_number | INT | DEFAULT 0 | เลขล่าสุดที่ถูกใช้งานไปแล้ว | +| updated_at | TIMESTAMP | ON UPDATE | เวลาที่อัปเดตล่าสุด | + +**Indexes**: + +- **PRIMARY KEY (project_id, originator_organization_id, recipient_organization_id, correspondence_type_id, sub_type_id, rfa_type_id, discipline_id, current_year)** + +**Business Rules**: + +- **Composite Primary Key 8 Columns**: เพื่อรองรับการรันเลขที่ซับซ้อนตาม Req 6B +- **Concurrency Control**: ใช้ Redis Lock หรือ Optimistic Locking ในการอัปเดต `last_number` +- **Reset Rule**: `current_year` เปลี่ยน -> เริ่มนับ 1 ใหม่ + +--- + +### 9.3 document_number_audit (NEW v1.5.1) + +**Purpose**: Audit log for document number generation (Debugging & Tracking) + +| Column Name | Data Type | Constraints | Description | +| :---------------- | :----------- | :---------- | :---------------------------------- | +| id | BIGINT | PK, AI | Unique ID | +| transaction_id | VARCHAR(36) | NOT NULL | UUID ของ Transaction การขอเลข | +| counter_key_json | JSON | NOT NULL | ค่า Key ที่ใช้ในการ Query (เก็บเป็น JSON) | +| generated_number | VARCHAR(100) | NOT NULL | เลขที่ได้ | +| requested_by | INT | FK | User ที่ขอเลข | +| requested_at | TIMESTAMP | DEFAULT NOW | เวลาที่ขอ | +| execution_time_ms | INT | NULL | เวลาที่ใช้ในการประมวลผล (ms) | + +--- + +### 9.4 document_number_errors (NEW v1.5.1) + +**Purpose**: Error log for failed document number generation + +| Column Name | Data Type | Constraints | Description | +| :--------------- | :---------- | :---------- | :------------------------------- | +| id | BIGINT | PK, AI | Unique ID | +| transaction_id | VARCHAR(36) | NOT NULL | UUID ของ Transaction | +| error_code | VARCHAR(50) | NOT NULL | รหัส Error (เช่น ERR_LOCK_TIMEOUT) | +| error_message | TEXT | NOT NULL | รายละเอียด Error | +| counter_key_json | JSON | NULL | ค่า Key ที่พยายามใช้ | +| occurred_at | TIMESTAMP | DEFAULT NOW | เวลาที่เกิด Error | + +--- + +## **10. ⚙️ Unified Workflow Engine Tables (NEW v1.5.1)** + +### 10.1 workflow_definitions + +**Purpose**: เก็บแม่แบบ (Template) ของ Workflow (Req 3.6) + +| Column Name | Data Type | Constraints | Description | +| :------------ | :----------- | :----------- | :--------------------------------------------- | +| id | INT | PK, AI | Unique ID | +| workflow_code | VARCHAR(50) | UNIQUE | รหัส Workflow (เช่น WF-RFA-GENERIC) | +| workflow_name | VARCHAR(255) | NOT NULL | ชื่อ Workflow | +| description | TEXT | NULL | คำอธิบาย | +| module | VARCHAR(50) | NOT NULL | ใช้กับ Module ไหน (RFA, CORRESPONDENCE) | +| steps_config | JSON | NOT NULL | การตั้งค่า Step (Sequence, Approvers, Conditions) | +| is_active | BOOLEAN | DEFAULT TRUE | สถานะการใช้งาน | +| version | INT | DEFAULT 1 | เวอร์ชันของ Definition | + +**Business Rules**: +- `steps_config` เก็บ Logic ของ Workflow ทั้งหมดในรูปแบบ JSON เพื่อความยืดหยุ่น + +--- + +### 10.2 workflow_instances + +**Purpose**: เก็บสถานะของ Workflow ที่กำลังรันอยู่จริง (Runtime) + +| Column Name | Data Type | Constraints | Description | +| :--------------------- | :----------- | :----------- | :----------------------------------------- | +| id | BIGINT | PK, AI | Unique ID | +| workflow_definition_id | INT | FK, NOT NULL | อ้างอิง Definition | +| business_key | VARCHAR(100) | INDEX | ID ของเอกสารที่ผูกกับ Workflow นี้ (เช่น RFA-001) | +| current_step_name | VARCHAR(100) | NOT NULL | ชื่อ Step ปัจจุบัน | +| status | ENUM | NOT NULL | IN_PROGRESS, COMPLETED, TERMINATED | +| context_data | JSON | NULL | ข้อมูลประกอบการตัดสินใจ (Variables) | +| started_at | TIMESTAMP | DEFAULT NOW | เวลาที่เริ่ม | +| completed_at | TIMESTAMP | NULL | เวลาที่จบ | + +--- + +### 10.3 workflow_histories + +**Purpose**: เก็บประวัติการดำเนินการในแต่ละ Step (Audit Trail) + +| Column Name | Data Type | Constraints | Description | +| :------------------- | :----------- | :----------- | :--------------------------------- | +| id | BIGINT | PK, AI | Unique ID | +| workflow_instance_id | BIGINT | FK, NOT NULL | อ้างอิง Instance | +| step_name | VARCHAR(100) | NOT NULL | ชื่อ Step | +| action | VARCHAR(50) | NOT NULL | การกระทำ (APPROVE, REJECT, COMMENT) | +| actor_id | INT | FK, NULL | User ที่กระทำ | +| comments | TEXT | NULL | ความเห็นเพิ่มเติม | +| performed_at | TIMESTAMP | DEFAULT NOW | เวลาที่กระทำ | + +--- + +## **11. 🖥️ System & Logs Tables (ระบบ, บันทึก)** + +### 11.1 json_schemas (NEW v1.5.1) + +**Purpose**: เก็บ Schema สำหรับ Validate JSON Columns (Req 3.12) + +| Column Name | Data Type | Constraints | Description | +| :---------- | :---------- | :----------- | :------------------------------------- | +| id | INT | PK, AI | Unique ID | +| schema_code | VARCHAR(50) | UNIQUE | รหัส Schema (เช่น RFA_DETAILS_V1) | +| schema_body | JSON | NOT NULL | JSON Schema Draft 7/2020-12 definition | +| description | TEXT | NULL | คำอธิบาย | +| is_active | BOOLEAN | DEFAULT TRUE | สถานะ | + +--- + +### 11.2 audit_logs (UPDATE v1.5.1) + +**Purpose**: Centralized audit logging for all system actions + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | --------------------------- | ----------------------------------------- | +| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique log ID | +| user_id | INT | NULL, FK | User who performed action | +| action | VARCHAR(50) | NOT NULL | Action name (CREATE, UPDATE, DELETE, etc) | +| module | VARCHAR(50) | NOT NULL | Module name (USERS, RFA, etc) | +| entity_id | VARCHAR(50) | NULL | ID of affected entity | +| old_values | JSON | NULL | Data before change | +| new_values | JSON | NULL | Data after change | +| ip_address | VARCHAR(45) | NULL | User IP address | +| user_agent | VARCHAR(255) | NULL | User browser/client info | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Log timestamp | + +**Indexes**: + +- PRIMARY KEY (id, created_at) -- **Partition Key** +- INDEX (user_id) +- INDEX (module) +- INDEX (action) +- INDEX (created_at) +- INDEX (entity_id) + +**Partitioning**: +- **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี เพื่อประสิทธิภาพในการเก็บข้อมูลระยะยาว + +--- + +### 11.3 notifications (UPDATE v1.5.1) + +**Purpose**: System notifications for users + +| Column Name | Data Type | Constraints | Description | +| ----------- | ------------ | --------------------------- | ----------------------------------- | +| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique notification ID | +| user_id | INT | NOT NULL, FK | Recipient user ID | +| title | VARCHAR(255) | NOT NULL | Notification title | +| message | TEXT | NOT NULL | Notification body | +| link | VARCHAR(500) | NULL | Action link URL | +| type | VARCHAR(50) | DEFAULT 'INFO' | Type: INFO, WARNING, ERROR, SUCCESS | +| is_read | BOOLEAN | DEFAULT FALSE | Read status | +| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Notification timestamp | + +**Indexes**: + +- PRIMARY KEY (id, created_at) -- **Partition Key** +- FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +- INDEX (user_id, is_read) +- INDEX (created_at) + +**Partitioning**: +- **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี + +--- + +## **12. 🔍 Views (มุมมองข้อมูล)** + +### 12.1 v_correspondence_details + +**Purpose**: Denormalized view for correspondence listing and searching + +```sql +CREATE VIEW v_correspondence_details AS +SELECT + c.id, + c.correspondence_number, + c.correspondence_type_id, + ct.type_name, + c.project_id, + p.project_code, + c.originator_id, + org.organization_code AS originator_code, + cr.title, + cr.revision_number, + cr.correspondence_status_id, + cs.status_name, + cr.document_date, + cr.created_at +FROM correspondences c +JOIN correspondence_revisions cr ON c.id = cr.correspondence_id AND cr.is_current = 1 +JOIN correspondence_types ct ON c.correspondence_type_id = ct.id +JOIN projects p ON c.project_id = p.id +LEFT JOIN organizations org ON c.originator_id = org.id +LEFT JOIN correspondence_status cs ON cr.correspondence_status_id = cs.id; +``` + +### 12.2 v_rfa_details + +**Purpose**: Denormalized view for RFA listing and searching + +```sql +CREATE VIEW v_rfa_details AS +SELECT + r.id, + c.correspondence_number AS rfa_number, + rt.type_code AS rfa_type, + rr.title, + rr.revision_number, + rsc.status_name AS rfa_status, + rac.approve_name AS approval_result, + rr.document_date, + rr.due_date +FROM rfas r +JOIN correspondences c ON r.id = c.id -- Assuming 1:1 mapping logic or shared ID +JOIN rfa_revisions rr ON r.id = rr.rfa_id AND rr.is_current = 1 +JOIN rfa_types rt ON r.rfa_type_id = rt.id +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; +``` + +--- + +## **13. 📊 Index Summaries (สรุป Index)** + +### 13.1 Performance Indexes + +| Table Name | Index Columns | Purpose | +| :----------------------- | :------------------------------------------------- | :----------------------------- | +| correspondences | (project_id, correspondence_number) | Fast lookup by document number | +| correspondences | (correspondence_type_id) | Filter by type | +| correspondence_revisions | (correspondence_id, is_current) | Get current revision | +| rfas | (rfa_type_id) | Filter by RFA type | +| rfa_revisions | (rfa_id, is_current) | Get current RFA revision | +| rfa_revisions | (rfa_status_code_id) | Filter by status | +| audit_logs | (created_at) | Date range queries | +| audit_logs | (user_id) | User activity history | +| audit_logs | (module, action) | Action type analysis | +| notifications | (user_id, is_read) | Unread notifications query | +| document_number_counters | (project_id, correspondence_type_id, current_year) | Running number generation | +| workflow_instances | (business_key) | Workflow lookup by document ID | +| workflow_instances | (status) | Monitor active workflows | + +### 13.2 Unique Constraints + +| Table Name | Columns | Description | +| :---------------------- | :----------------------------------- | :--------------------------------- | +| users | (username) | Unique login name | +| users | (email) | Unique email address | +| organizations | (organization_code) | Unique organization code | +| projects | (project_code) | Unique project code | +| contracts | (contract_code) | Unique contract code | +| correspondences | (project_id, correspondence_number) | Unique document number per project | +| rfas | (drawing_number) | Unique shop drawing number | +| document_number_formats | (project_id, correspondence_type_id) | One format per type per project | +| workflow_definitions | (workflow_code) | Unique workflow code | + +--- + +## **14. 🛡️ Data Integrity Constraints (ความถูกต้องของข้อมูล)** + +### 14.1 Soft Delete Policy + +- **Tables with `deleted_at`**: + - users + - organizations + - projects + - contracts + - correspondences + - rfas + - shop_drawings + - contract_drawings +- **Rule**: Records are never physically deleted. `deleted_at` is set to timestamp. +- **Query Rule**: All standard queries MUST include `WHERE deleted_at IS NULL`. + +### 14.2 Foreign Key Cascades + +- **ON DELETE CASCADE**: + - Used for child tables that cannot exist without parent (e.g., `correspondence_revisions`, `rfa_revisions`, `correspondence_attachments`). +- **ON DELETE RESTRICT**: + - Used for master data references to prevent accidental deletion of used data (e.g., `correspondence_types`, `organizations`). +- **ON DELETE SET NULL**: + - Used for optional references (e.g., `created_by`, `originator_id`). + +--- + +## **15. 🔐 Security & Permissions Model (ความปลอดภัย)** + +### 15.1 Row-Level Security (RLS) Logic + +- **Organization Scope**: Users can only see documents where `originator_id` OR `recipient_organization_id` matches their organization. +- **Project Scope**: Users can only see documents within projects they are assigned to. +- **Confidentiality**: Documents marked `is_confidential` are visible ONLY to specific roles or users. + +### 15.2 Role-Based Access Control (RBAC) + +- **Permissions** are granular (e.g., `correspondence.view`, `correspondence.create`). +- **Roles** aggregate permissions (e.g., `Document Controller` = `view` + `create` + `edit`). +- **Assignments** link Users to Roles within a Context (Global, Project, or Organization). + +--- + +## **16. 🔄 Data Migration & Seeding (การย้ายข้อมูล)** + +### 16.1 Initial Seeding (V1.5.1) + +1. **Master Data**: + - `organizations`: Owner, Consultant, Contractor + - `projects`: LCBP3 + - `correspondence_types`: LETTER, MEMO, TRANSMITTAL, RFA + - `rfa_types`: DWG, MAT, DOC, RFI + - `rfa_status_codes`: DFT, PEND, APPR, REJ + - `disciplines`: GEN, STR, ARC, MEP (New V1.5.1) +2. **System Users**: + - `admin`: Super Admin + - `system`: System Bot for automated tasks + +### 16.2 Migration Strategy + +- **Schema Migration**: Use TypeORM Migrations or raw SQL scripts (versioned). +- **Data Migration**: + - **V1.4.5 -> V1.5.1**: + - Run SQL script `8_lcbp3_v1_5_1.sql` + - Populate `disciplines` table. + - Update `document_number_counters` PK (Requires careful migration of existing counters). + - Initialize `workflow_definitions`. + +--- + +## **17. 📈 Monitoring & Maintenance (การดูแลรักษา)** + +### 17.1 Database Maintenance + +- **Daily**: Incremental Backup. +- **Weekly**: Full Backup + `OPTIMIZE TABLE` for heavy tables (`audit_logs`, `notifications`). +- **Monthly**: Archive old `audit_logs` partitions to cold storage. + +### 17.2 Health Checks + +- Monitor `document_number_errors` for numbering failures. +- Monitor `workflow_instances` for stuck workflows (`status = 'IN_PROGRESS'` > 7 days). +- Check `document_number_counters` for gaps or resets. + +--- + +## **18. 📖 Glossary (คำศัพท์)** + +- **RFA**: Request for Approval (เอกสารขออนุมัติ) +- **Transmittal**: Document Transmittal Sheet (ใบนำส่งเอกสาร) +- **Shop Drawing**: แบบก่อสร้างที่ผู้รับเหมาจัดทำ +- **Contract Drawing**: แบบสัญญา (แบบตั้งต้น) +- **Revision**: ฉบับแก้ไข (0, 1, 2, A, B, C) +- **Originator**: ผู้จัดทำ/ผู้ส่งเอกสาร +- **Recipient**: ผู้รับเอกสาร +- **Workflow**: กระบวนการทำงาน/อนุมัติ +- **Discipline**: สาขางาน (เช่น โยธา, สถาปัตย์, ไฟฟ้า) + +--- + +**End of Data Dictionary V1.5.1** + + + diff --git a/docs/8_lcbp3_v1_5_1.sql b/docs/8_lcbp3_v1_5_1.sql index 8a82c37..b8458db 100644 --- a/docs/8_lcbp3_v1_5_1.sql +++ b/docs/8_lcbp3_v1_5_1.sql @@ -22,6 +22,7 @@ SET NAMES utf8mb4; SET time_zone = '+07:00'; + -- ปิดการตรวจสอบ Foreign Key ชั่วคราวเพื่อให้สามารถลบตารางได้ทั้งหมด SET FOREIGN_KEY_CHECKS = 0; @@ -40,10 +41,12 @@ DROP VIEW IF EXISTS v_contract_parties_all; DROP VIEW IF EXISTS v_current_rfas; DROP VIEW IF EXISTS v_current_correspondences; + -- DROP PROCEDURE IF EXISTS sp_get_next_document_number; -- 🗑️ DROP TABLE SCRIPT: LCBP3-DMS v1.4.2 -- คำเตือน: ข้อมูลทั้งหมดจะหายไป กรุณา Backup ก่อนรันบน Production SET FOREIGN_KEY_CHECKS = 0; + -- ============================================================ -- ส่วนที่ 1: ตาราง System, Logs & Preferences (ตารางปลายทาง/ส่วนเสริม) -- ============================================================ @@ -54,14 +57,18 @@ DROP TABLE IF EXISTS search_indices; DROP TABLE IF EXISTS notifications; DROP TABLE IF EXISTS audit_logs; + -- [NEW v1.4.2] ตารางการตั้งค่าส่วนตัวของผู้ใช้ (FK -> users) DROP TABLE IF EXISTS user_preferences; + -- [NEW v1.4.2] ตารางเก็บ Schema สำหรับ Validate JSON (Stand-alone) DROP TABLE IF EXISTS json_schemas; + -- [v1.5.1 NEW] ตาราง Audit และ Error Log สำหรับ Document Numbering DROP TABLE IF EXISTS document_number_errors; DROP TABLE IF EXISTS document_number_audit; + -- ============================================================ -- ส่วนที่ 2: ตาราง Junction (เชื่อมโยงข้อมูล M:N) -- ============================================================ @@ -70,6 +77,7 @@ DROP TABLE IF EXISTS correspondence_tags; DROP TABLE IF EXISTS shop_drawing_revision_contract_refs; DROP TABLE IF EXISTS contract_drawing_subcat_cat_maps; + -- ============================================================ -- ส่วนที่ 3: ตารางไฟล์แนบและการเชื่อมโยง (Attachments) -- ============================================================ @@ -82,6 +90,7 @@ DROP TABLE IF EXISTS shop_drawing_revision_attachments; DROP TABLE IF EXISTS correspondence_attachments; DROP TABLE IF EXISTS attachments; + -- ตารางหลักเก็บ path ไฟล์ -- ============================================================ -- ส่วนที่ 4: ตาราง Workflow & Routing (Process Logic) @@ -97,6 +106,7 @@ DROP TABLE IF EXISTS user_assignments; DROP TABLE IF EXISTS contract_organizations; DROP TABLE IF EXISTS project_organizations; + -- ============================================================ -- ส่วนที่ 6: ตารางรายละเอียดของเอกสาร (Revisions & Items) -- ============================================================ @@ -113,6 +123,7 @@ DROP TABLE IF EXISTS correspondence_references; DROP TABLE IF EXISTS correspondence_recipients; DROP TABLE IF EXISTS correspondence_revisions; + -- [Modified v1.4.2] มี Virtual Columns -- ============================================================ -- ส่วนที่ 7: ตารางเอกสารหลัก (Core Documents) @@ -128,6 +139,7 @@ DROP TABLE IF EXISTS shop_drawings; DROP TABLE IF EXISTS rfas; DROP TABLE IF EXISTS correspondences; + -- ============================================================ -- ส่วนที่ 8: ตารางหมวดหมู่และข้อมูลหลัก (Master Data) -- ============================================================ @@ -159,10 +171,12 @@ DROP TABLE IF EXISTS correspondence_status; DROP TABLE IF EXISTS correspondence_types; DROP TABLE IF EXISTS document_number_counters; + -- [Modified v1.4.2] มี version column DROP TABLE IF EXISTS document_number_formats; DROP TABLE IF EXISTS tags; + -- ============================================================ -- ส่วนที่ 9: ตารางผู้ใช้ บทบาท และโครงสร้างรากฐาน (Root Tables) -- ============================================================ @@ -177,595 +191,647 @@ DROP TABLE IF EXISTS contracts; DROP TABLE IF EXISTS projects; DROP TABLE IF EXISTS users; + -- Referenced by user_preferences, audit_logs, etc. DROP TABLE IF EXISTS organizations; + -- Referenced by users, projects, etc. -- ===================================================== -- 1. 🏢 Core & Master Data (องค์กร, โครงการ, สัญญา) -- ===================================================== -- ตาราง Master เก็บประเภทบทบาทขององค์กร CREATE TABLE organization_roles ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - role_name VARCHAR(20) NOT NULL UNIQUE COMMENT 'ชื่อบทบาท (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD PARTY)' + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + role_name VARCHAR(20) NOT NULL UNIQUE COMMENT 'ชื่อบทบาท (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD PARTY)' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บประเภทบทบาทขององค์กร'; + -- ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ CREATE TABLE organizations ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - organization_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสองค์กร', - organization_name VARCHAR(255) NOT NULL COMMENT 'ชื่อองค์กร', - role_id INT COMMENT 'บทบาทขององค์กร', - is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - FOREIGN KEY (role_id) REFERENCES organization_roles (id) ON DELETE SET NULL + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + organization_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสองค์กร', + organization_name VARCHAR(255) NOT NULL COMMENT 'ชื่อองค์กร', + role_id INT COMMENT 'บทบาทขององค์กร', + is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (role_id) REFERENCES organization_roles (id) ON DELETE + SET NULL ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ'; + -- ตาราง Master เก็บข้อมูลโครงการ CREATE TABLE projects ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - project_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสโครงการ', - project_name VARCHAR(255) NOT NULL COMMENT 'ชื่อโครงการ', - -- parent_project_id INT COMMENT 'รหัสโครงการหลัก (ถ้ามี)', - -- contractor_organization_id INT COMMENT 'รหัสองค์กรผู้รับเหมา (ถ้ามี)', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน' -- FOREIGN KEY (parent_project_id) REFERENCES projects(id) ON DELETE SET NULL, - -- FOREIGN KEY (contractor_organization_id) REFERENCES organizations(id) ON DELETE SET NULL + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสโครงการ', + project_name VARCHAR(255) NOT NULL COMMENT 'ชื่อโครงการ', + -- parent_project_id INT COMMENT 'รหัสโครงการหลัก (ถ้ามี)', + -- contractor_organization_id INT COMMENT 'รหัสองค์กรผู้รับเหมา (ถ้ามี)', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน' -- FOREIGN KEY (parent_project_id) REFERENCES projects(id) ON DELETE SET NULL, + -- FOREIGN KEY (contractor_organization_id) REFERENCES organizations(id) ON DELETE SET NULL ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลโครงการ'; + -- ตาราง Master เก็บข้อมูลสัญญา CREATE TABLE contracts ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - project_id INT NOT NULL, - contract_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสัญญา', - contract_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสัญญา', - description TEXT COMMENT 'คำอธิบายสัญญา', - start_date DATE COMMENT 'วันที่เริ่มสัญญา', - end_date DATE COMMENT 'วันที่สิ้นสุดสัญญา', - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL, + contract_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสัญญา', + contract_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสัญญา', + description TEXT COMMENT 'คำอธิบายสัญญา', + start_date DATE COMMENT 'วันที่เริ่มสัญญา', + end_date DATE COMMENT 'วันที่สิ้นสุดสัญญา', + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลสัญญา'; + -- ===================================================== -- 2. 👥 Users & RBAC (ผู้ใช้, สิทธิ์, บทบาท) -- ===================================================== -- ตาราง Master เก็บข้อมูลผู้ใช้งาน (User) CREATE TABLE users ( - user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - username VARCHAR(50) NOT NULL UNIQUE COMMENT 'ชื่อผู้ใช้งาน', - password_hash VARCHAR(255) NOT NULL COMMENT 'รหัสผ่าน (Hashed)', - first_name VARCHAR(50) COMMENT 'ชื่อจริง', - last_name VARCHAR(50) COMMENT 'นามสกุล', - email VARCHAR(100) NOT NULL UNIQUE COMMENT 'อีเมล', - line_id VARCHAR(100) COMMENT 'LINE ID', - primary_organization_id INT COMMENT 'สังกัดองค์กร', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', - failed_attempts INT DEFAULT 0 COMMENT 'จำนวนครั้งที่ล็อกอินล้มเหลว', - locked_until DATETIME COMMENT 'ล็อกอินไม่ได้จนถึงเวลา', - last_login_at TIMESTAMP NULL COMMENT 'วันที่และเวลาที่ล็อกอินล่าสุด', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - deleted_at DATETIME NULL DEFAULT NULL COMMENT 'วันที่ลบ', - FOREIGN KEY (primary_organization_id) REFERENCES organizations (id) ON DELETE SET NULL + user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + username VARCHAR(50) NOT NULL UNIQUE COMMENT 'ชื่อผู้ใช้งาน', + password_hash VARCHAR(255) NOT NULL COMMENT 'รหัสผ่าน (Hashed)', + first_name VARCHAR(50) COMMENT 'ชื่อจริง', + last_name VARCHAR(50) COMMENT 'นามสกุล', + email VARCHAR(100) NOT NULL UNIQUE COMMENT 'อีเมล', + line_id VARCHAR(100) COMMENT 'LINE ID', + primary_organization_id INT COMMENT 'สังกัดองค์กร', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + failed_attempts INT DEFAULT 0 COMMENT 'จำนวนครั้งที่ล็อกอินล้มเหลว', + locked_until DATETIME COMMENT 'ล็อกอินไม่ได้จนถึงเวลา', + last_login_at TIMESTAMP NULL COMMENT 'วันที่และเวลาที่ล็อกอินล่าสุด', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL DEFAULT NULL COMMENT 'วันที่ลบ', + FOREIGN KEY (primary_organization_id) REFERENCES organizations (id) ON DELETE + SET NULL ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)'; + -- ตาราง Master เก็บ "บทบาท" ของผู้ใช้ในระบบ CREATE TABLE roles ( - role_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - -- role_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสบทบาท (เช่น SUPER_ADMIN, ADMIN, EDITOR, VIEWER)', - role_name VARCHAR(100) NOT NULL COMMENT 'ชื่อบทบาท', - scope ENUM( - 'Global', - 'Organization', - 'Project', - 'Contract' - ) NOT NULL, - -- ขอบเขตของบทบาท (จากข้อ 4.3) - description TEXT COMMENT 'คำอธิบายบทบาท', - is_system BOOLEAN DEFAULT FALSE COMMENT '(1 = บทบาทของระบบ ลบไม่ได้)' + role_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + -- role_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสบทบาท (เช่น SUPER_ADMIN, ADMIN, EDITOR, VIEWER)', + role_name VARCHAR(100) NOT NULL COMMENT 'ชื่อบทบาท', + scope ENUM( + 'Global', + 'Organization', + 'Project', + 'Contract' + ) NOT NULL, + -- ขอบเขตของบทบาท (จากข้อ 4.3) + description TEXT COMMENT 'คำอธิบายบทบาท', + is_system BOOLEAN DEFAULT FALSE COMMENT '(1 = บทบาทของระบบ ลบไม่ได้)' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "บทบาท" ของผู้ใช้ในระบบ'; + -- ตาราง Master เก็บ "สิทธิ์" (Permission) หรือ "การกระทำ" ทั้งหมดในระบบ CREATE TABLE permissions ( - permission_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - permission_name VARCHAR(100) NOT NULL UNIQUE COMMENT 'รหัสสิทธิ์ (เช่น rfas.create, rfas.view)', - description TEXT COMMENT 'คำอธิบายสิทธิ์', - module VARCHAR(50) COMMENT 'โมดูลที่เกี่ยวข้อง', - scope_level ENUM('GLOBAL', 'ORG', 'PROJECT') COMMENT 'ระดับขอบเขตของสิทธิ์', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน' + permission_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + permission_name VARCHAR(100) NOT NULL UNIQUE COMMENT 'รหัสสิทธิ์ (เช่น rfas.create, rfas.view)', + description TEXT COMMENT 'คำอธิบายสิทธิ์', + module VARCHAR(50) COMMENT 'โมดูลที่เกี่ยวข้อง', + scope_level ENUM('GLOBAL', 'ORG', 'PROJECT') COMMENT 'ระดับขอบเขตของสิทธิ์', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "สิทธิ์" (Permission) หรือ "การกระทำ" ทั้งหมดในระบบ'; + -- ตารางเชื่อมระหว่าง roles และ permissions (M:N) CREATE TABLE role_permissions ( - role_id INT COMMENT 'ID ของบทบาท', - permission_id INT COMMENT 'ID ของสิทธิ์', - PRIMARY KEY (role_id, permission_id), - FOREIGN KEY (role_id) REFERENCES roles (role_id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES permissions (permission_id) ON DELETE CASCADE + role_id INT COMMENT 'ID ของบทบาท', + permission_id INT COMMENT 'ID ของสิทธิ์', + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES roles (role_id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions (permission_id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง roles และ permissions (M :N)'; + -- search.advanced -- ตารางเชื่อมผู้ใช้ (users) CREATE TABLE user_assignments ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - role_id INT NOT NULL, - -- คอลัมน์สำหรับกำหนดขอบเขต (จะใช้เพียงอันเดียวต่อแถว) - organization_id INT NULL, - project_id INT NULL, - contract_id INT NULL, - assigned_by_user_id INT, - -- ผู้ที่มอบหมายบทบาทนี้ - assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE, - FOREIGN KEY (role_id) REFERENCES roles (role_id) ON DELETE CASCADE, - FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE, - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE, - FOREIGN KEY (assigned_by_user_id) REFERENCES users (user_id), - -- Constraint เพื่อให้แน่ใจว่ามีเพียงขอบเขตเดียวที่ถูกกำหนดในแต่ละแถว - CONSTRAINT chk_scope CHECK ( - ( - organization_id IS NOT NULL - AND project_id IS NULL - AND contract_id IS NULL - ) - OR ( - organization_id IS NULL - AND project_id IS NOT NULL - AND contract_id IS NULL - ) - OR ( - organization_id IS NULL - AND project_id IS NULL - AND contract_id IS NOT NULL - ) - OR ( - organization_id IS NULL - AND project_id IS NULL - AND contract_id IS NULL - ) -- สำหรับ Global scope + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + role_id INT NOT NULL, + -- คอลัมน์สำหรับกำหนดขอบเขต (จะใช้เพียงอันเดียวต่อแถว) + organization_id INT NULL, + project_id INT NULL, + contract_id INT NULL, + assigned_by_user_id INT, + -- ผู้ที่มอบหมายบทบาทนี้ + assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles (role_id) ON DELETE CASCADE, + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE, + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE, + FOREIGN KEY (assigned_by_user_id) REFERENCES users (user_id), + -- Constraint เพื่อให้แน่ใจว่ามีเพียงขอบเขตเดียวที่ถูกกำหนดในแต่ละแถว + CONSTRAINT chk_scope CHECK ( + ( + organization_id IS NOT NULL + AND project_id IS NULL + AND contract_id IS NULL ) + OR ( + organization_id IS NULL + AND project_id IS NOT NULL + AND contract_id IS NULL + ) + OR ( + organization_id IS NULL + AND project_id IS NULL + AND contract_id IS NOT NULL + ) + OR ( + organization_id IS NULL + AND project_id IS NULL + AND contract_id IS NULL + ) -- สำหรับ Global scope + ) ); CREATE TABLE project_organizations ( - project_id INT NOT NULL, - organization_id INT NOT NULL, - PRIMARY KEY (project_id, organization_id), - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE + project_id INT NOT NULL, + organization_id INT NOT NULL, + PRIMARY KEY (project_id, organization_id), + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE ); CREATE TABLE contract_organizations ( - contract_id INT NOT NULL, - organization_id INT NOT NULL, - role_in_contract VARCHAR(100), - -- เช่น 'Owner', 'Designer', 'Consultant', 'Contractor ' - PRIMARY KEY (contract_id, organization_id), - FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE, - FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE + contract_id INT NOT NULL, + organization_id INT NOT NULL, + role_in_contract VARCHAR(100), + -- เช่น 'Owner', 'Designer', 'Consultant', 'Contractor ' + PRIMARY KEY (contract_id, organization_id), + FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE, + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE ); + -- ===================================================== -- 3. ✉️ Correspondences (เอกสารหลัก, Revisions) -- ===================================================== -- ตาราง Master เก็บประเภทเอกสารโต้ตอบ CREATE TABLE correspondence_types ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - type_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสประเภท (เช่น RFA, RFI)', - type_name VARCHAR(255) NOT NULL COMMENT 'ชื่อประเภท', - sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + type_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสประเภท (เช่น RFA, RFI)', + type_name VARCHAR(255) NOT NULL COMMENT 'ชื่อประเภท', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บประเภทเอกสารโต้ตอบ'; + -- ตาราง Master เก็บสถานะของเอกสาร CREATE TABLE correspondence_status ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - status_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสถานะหนังสือ (เช่น DRAFT, SUBOWN)', - status_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสถานะหนังสือ', - sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + status_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสถานะหนังสือ (เช่น DRAFT, SUBOWN)', + status_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสถานะหนังสือ', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บสถานะของเอกสาร'; + -- ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision CREATE TABLE correspondences ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง (นี่คือ "Master ID" ที่ใช้เชื่อมโยง)', - correspondence_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสาร (สร้างจาก DocumentNumberingModule)', - correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร', - is_internal_communication TINYINT(1) DEFAULT 0 COMMENT '(1 = ภายใน, 0 = ภายนอก)', - project_id INT NOT NULL COMMENT 'อยู่ในโครงการ', - originator_id INT COMMENT 'องค์กรผู้ส่ง', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - created_by INT COMMENT 'ผู้สร้าง', - deleted_at DATETIME NULL COMMENT 'สำหรับ Soft Delete', - FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE RESTRICT, - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - FOREIGN KEY (originator_id) REFERENCES organizations (id) ON DELETE SET NULL, - FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE SET NULL, + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง (นี่คือ "Master ID" ที่ใช้เชื่อมโยง)', + correspondence_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสาร (สร้างจาก DocumentNumberingModule)', + correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร', + is_internal_communication TINYINT(1) DEFAULT 0 COMMENT '(1 = ภายใน, 0 = ภายนอก)', + project_id INT NOT NULL COMMENT 'อยู่ในโครงการ', + originator_id INT COMMENT 'องค์กรผู้ส่ง', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + created_by INT COMMENT 'ผู้สร้าง', + deleted_at DATETIME NULL COMMENT 'สำหรับ Soft Delete', + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE RESTRICT, + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (originator_id) REFERENCES organizations (id) ON DELETE + SET NULL, + FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE + SET NULL, UNIQUE KEY uq_corr_no_per_project ( - project_id, - correspondence_number + project_id, + correspondence_number ) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision'; + -- ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1:N) CREATE TABLE correspondence_revisions ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', - correspondence_id INT NOT NULL COMMENT 'Master ID', - 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 ปัจจุบัน)', - correspondence_status_id INT NOT NULL COMMENT 'สถานะของ Revision นี้', - title VARCHAR(255) NOT NULL COMMENT 'เรื่อง', - document_date DATE COMMENT 'วันที่ในเอกสาร', - issued_date DATETIME COMMENT 'วันที่ออกเอกสาร', - received_date DATETIME COMMENT 'วันที่ลงรับเอกสาร', - due_date DATETIME COMMENT 'วันที่ครบกำหนด', - description TEXT COMMENT 'คำอธิบายการแก้ไขใน Revision นี้', - details JSON COMMENT 'ข้อมูลเฉพาะ (เช่น RFI details)', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้างเอกสาร', - created_by INT COMMENT 'ผู้สร้าง', - updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', - FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, - FOREIGN KEY (correspondence_status_id) REFERENCES correspondence_status (id) ON DELETE RESTRICT, - FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE SET NULL, - FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE SET NULL, + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', + correspondence_id INT NOT NULL COMMENT 'Master ID', + 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 ปัจจุบัน)', + correspondence_status_id INT NOT NULL COMMENT 'สถานะของ Revision นี้', + title VARCHAR(255) NOT NULL COMMENT 'เรื่อง', + document_date DATE COMMENT 'วันที่ในเอกสาร', + issued_date DATETIME COMMENT 'วันที่ออกเอกสาร', + received_date DATETIME COMMENT 'วันที่ลงรับเอกสาร', + due_date DATETIME COMMENT 'วันที่ครบกำหนด', + description TEXT COMMENT 'คำอธิบายการแก้ไขใน Revision นี้', + details JSON COMMENT 'ข้อมูลเฉพาะ (เช่น RFI details)', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้างเอกสาร', + created_by INT COMMENT 'ผู้สร้าง', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_status_id) REFERENCES correspondence_status (id) ON DELETE RESTRICT, + 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 uq_master_revision_number ( - correspondence_id, - revision_number + correspondence_id, + revision_number ), UNIQUE KEY uq_master_current (correspondence_id, is_current) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1 :N)'; + -- ตารางเชื่อมผู้รับ (TO/CC) สำหรับเอกสารแต่ละฉบับ (M:N) CREATE TABLE correspondence_recipients ( - correspondence_id INT COMMENT 'ID ของเอกสาร', - recipient_organization_id INT COMMENT 'ID องค์กรผู้รับ', - recipient_type ENUM('TO', 'CC ') COMMENT 'ประเภทผู้รับ (TO หรือ CC)', - PRIMARY KEY ( - correspondence_id, - recipient_organization_id, - recipient_type - ), - FOREIGN KEY (correspondence_id) REFERENCES correspondence_revisions (correspondence_id) ON DELETE CASCADE, - FOREIGN KEY (recipient_organization_id) REFERENCES organizations (id) ON DELETE RESTRICT + correspondence_id INT COMMENT 'ID ของเอกสาร', + recipient_organization_id INT COMMENT 'ID องค์กรผู้รับ', + recipient_type ENUM('TO', 'CC ') COMMENT 'ประเภทผู้รับ (TO หรือ CC)', + PRIMARY KEY ( + correspondence_id, + recipient_organization_id, + recipient_type + ), + FOREIGN KEY (correspondence_id) REFERENCES correspondence_revisions (correspondence_id) ON DELETE CASCADE, + FOREIGN KEY (recipient_organization_id) REFERENCES organizations (id) ON DELETE RESTRICT ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมผู้รับ (TO / CC) สำหรับเอกสารแต่ละฉบับ (M :N)'; + -- ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ CREATE TABLE tags ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - tag_name VARCHAR(100) NOT NULL UNIQUE COMMENT 'ชื่อ Tag', - description TEXT COMMENT 'คำอธิบายแท็ก', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด ' + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + tag_name VARCHAR(100) NOT NULL UNIQUE COMMENT 'ชื่อ Tag', + description TEXT COMMENT 'คำอธิบายแท็ก', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด ' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ'; + -- ตารางเชื่อมระหว่าง correspondences และ tags (M:N) CREATE TABLE correspondence_tags ( - correspondence_id INT COMMENT 'ID ของเอกสาร', - tag_id INT COMMENT 'ID ของ Tag', - PRIMARY KEY (correspondence_id, tag_id), - FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, - FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE + correspondence_id INT COMMENT 'ID ของเอกสาร', + tag_id INT COMMENT 'ID ของ Tag', + PRIMARY KEY (correspondence_id, tag_id), + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง correspondences และ tags (M :N)'; + -- ตารางเชื่อมการอ้างอิงระหว่างเอกสาร (M:N) CREATE TABLE correspondence_references ( - src_correspondence_id INT COMMENT 'ID เอกสารต้นทาง', - tgt_correspondence_id INT COMMENT 'ID เอกสารเป้าหมาย', - PRIMARY KEY ( - src_correspondence_id, - tgt_correspondence_id - ), - FOREIGN KEY (src_correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, - FOREIGN KEY (tgt_correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE + src_correspondence_id INT COMMENT 'ID เอกสารต้นทาง', + tgt_correspondence_id INT COMMENT 'ID เอกสารเป้าหมาย', + PRIMARY KEY ( + src_correspondence_id, + tgt_correspondence_id + ), + FOREIGN KEY (src_correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (tgt_correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมการอ้างอิงระหว่างเอกสาร (M :N)'; + -- ===================================================== -- 4. 📐 approval: RFA (เอกสารขออนุมัติ, Workflows) -- ===================================================== -- ตาราง Master สำหรับประเภท RFA CREATE TABLE rfa_types ( - id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 'ID ของตาราง', - contract_id INT NOT NULL COMMENT 'ผูกกับสัญญา', - type_code VARCHAR(20) NOT NULL COMMENT 'รหัสประเภท RFA (เช่น DWG, DOC, MAT)', - type_name_th VARCHAR(100) NOT NULL COMMENT 'ชื่อประเภท RFA th', - type_name_en VARCHAR(100) NOT NULL COMMENT 'ชื่อประเภท RFA en', - remark TEXT COMMENT 'หมายเหตุ', - -- sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ', - UNIQUE KEY uk_rfa_types_contract_code (contract_id, type_code), - FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE + id INT PRIMARY KEY NOT NULL AUTO_INCREMENT COMMENT 'ID ของตาราง', + contract_id INT NOT NULL COMMENT 'ผูกกับสัญญา', + type_code VARCHAR(20) NOT NULL COMMENT 'รหัสประเภท RFA (เช่น DWG, DOC, MAT)', + type_name_th VARCHAR(100) NOT NULL COMMENT 'ชื่อประเภท RFA th', + type_name_en VARCHAR(100) NOT NULL COMMENT 'ชื่อประเภท RFA en', + remark TEXT COMMENT 'หมายเหตุ', + -- sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ', + UNIQUE KEY uk_rfa_types_contract_code (contract_id, type_code), + FOREIGN KEY (contract_id) REFERENCES contracts (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับประเภท RFA'; + -- [NEW 6B] ตารางเก็บข้อมูลสาขางาน (Disciplines) แยกตามสัญญา CREATE TABLE 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) + 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'; + -- [NEW 6B] ตารางเก็บประเภทหนังสือย่อย (Sub Types) สำหรับ Mapping เลขรหัส CREATE TABLE 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 + 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'; + -- หรือใช้ ALTER TABLE (แนะนำให้รันหลังสร้างตาราง disciplines เสร็จ) ALTER TABLE correspondences -ADD COLUMN discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)' AFTER correspondence_type_id; +ADD COLUMN discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)' +AFTER correspondence_type_id; ALTER TABLE correspondences -ADD CONSTRAINT fk_corr_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE SET NULL; +ADD CONSTRAINT fk_corr_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE +SET NULL; + -- ตาราง Master สำหรับสถานะ RFA CREATE TABLE rfa_status_codes ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - status_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสสถานะ RFA (เช่น DFT - Draft, FAP - For Approve)', - status_name VARCHAR(100) NOT NULL COMMENT 'ชื่อสถานะ', - description TEXT COMMENT 'คำอธิบาย', - sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + status_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสสถานะ RFA (เช่น DFT - Draft, FAP - For Approve)', + status_name VARCHAR(100) NOT NULL COMMENT 'ชื่อสถานะ', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับสถานะ RFA'; + -- ตาราง Master สำหรับรหัสผลการอนุมัติ RFA CREATE TABLE rfa_approve_codes ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - approve_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสผลการอนุมัติ ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + approve_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสผลการอนุมัติ ( เช่น 1A - Approved, 3R - Revise and Resubmit )', - approve_name VARCHAR(100) NOT NULL COMMENT 'ชื่อผลการอนุมัติ', - description TEXT COMMENT 'คำอธิบาย', - sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' + approve_name VARCHAR(100) NOT NULL COMMENT 'ชื่อผลการอนุมัติ', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับรหัสผลการอนุมัติ RFA'; CREATE TABLE rfas ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง (RFA Master ID)', - rfa_type_id INT NOT NULL COMMENT 'ประเภท RFA', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - created_by INT COMMENT 'ผู้สร้าง', - deleted_at DATETIME NULL COMMENT 'สำหรับ Soft Delete', - FOREIGN KEY (rfa_type_id) REFERENCES rfa_types (id), - FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE SET NULL + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง (RFA Master ID)', + rfa_type_id INT NOT NULL COMMENT 'ประเภท RFA', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + created_by INT COMMENT 'ผู้สร้าง', + deleted_at DATETIME NULL COMMENT 'สำหรับ Soft Delete', + FOREIGN KEY (rfa_type_id) REFERENCES rfa_types (id), + FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE + SET NULL ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของ RFA (มีความสัมพันธ์ 1 :N กับ rfa_revisions)'; ALTER TABLE rfas -ADD COLUMN discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)' AFTER rfa_type_id; +ADD COLUMN discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)' +AFTER rfa_type_id; ALTER TABLE rfas -ADD CONSTRAINT fk_rfa_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE SET NULL; +ADD CONSTRAINT fk_rfa_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE +SET NULL; + -- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ rfas (1:N) CREATE TABLE rfa_revisions ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', - correspondence_id INT NOT NULL COMMENT 'Master ID ของ Correspondence', - 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_status_code_id INT NOT NULL COMMENT 'สถานะ RFA', - rfa_approve_code_id INT COMMENT 'ผลการอนุมัติ', - title VARCHAR(255) NOT NULL COMMENT 'เรื่อง', - document_date DATE COMMENT 'วันที่ในเอกสาร', - issued_date DATE COMMENT 'วันที่ส่งขออนุมัติ', - received_date DATETIME COMMENT 'วันที่ลงรับเอกสาร', - approved_date DATE COMMENT 'วันที่อนุมัติ', - description TEXT COMMENT 'คำอธิบายการแก้ไขใน Revision นี้', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้างเอกสาร', - created_by INT COMMENT 'ผู้สร้าง', - updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', - FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, - FOREIGN KEY (rfa_id) REFERENCES rfas (id) ON DELETE CASCADE, - 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 (created_by) REFERENCES users (user_id) ON DELETE SET NULL, - FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE SET NULL, + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', + correspondence_id INT NOT NULL COMMENT 'Master ID ของ Correspondence', + 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_status_code_id INT NOT NULL COMMENT 'สถานะ RFA', + rfa_approve_code_id INT COMMENT 'ผลการอนุมัติ', + title VARCHAR(255) NOT NULL COMMENT 'เรื่อง', + document_date DATE COMMENT 'วันที่ในเอกสาร', + issued_date DATE COMMENT 'วันที่ส่งขออนุมัติ', + received_date DATETIME COMMENT 'วันที่ลงรับเอกสาร', + approved_date DATE COMMENT 'วันที่อนุมัติ', + description TEXT COMMENT 'คำอธิบายการแก้ไขใน Revision นี้', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้างเอกสาร', + created_by INT COMMENT 'ผู้สร้าง', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (rfa_id) REFERENCES rfas (id) ON DELETE CASCADE, + 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 (created_by) REFERENCES users (user_id) ON DELETE + 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) CREATE TABLE 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 - ), - FOREIGN KEY (rfarev_correspondence_id) REFERENCES rfa_revisions (correspondence_id) ON DELETE CASCADE, - FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions (id) ON DELETE CASCADE + 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 + ), + FOREIGN KEY (rfarev_correspondence_id) REFERENCES rfa_revisions (correspondence_id) ON DELETE CASCADE, + FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง rfa_revisions (ที่เป็นประเภท DWG) กับ shop_drawing_revisions (M :N)'; + -- ===================================================== -- 5. 📐 Drawings (แบบ, หมวดหมู่) -- ===================================================== -- ตาราง Master สำหรับ "เล่ม" ของแบบคู่สัญญา CREATE TABLE contract_drawing_volumes ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - project_id INT NOT NULL COMMENT 'โครงการ', - volume_code VARCHAR(50) NOT NULL COMMENT 'รหัสเล่ม', - volume_name VARCHAR(255) NOT NULL COMMENT 'ชื่อเล่ม', - description TEXT COMMENT 'คำอธิบาย', - sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - UNIQUE KEY ux_volume_project (project_id, volume_code) + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + volume_code VARCHAR(50) NOT NULL COMMENT 'รหัสเล่ม', + volume_name VARCHAR(255) NOT NULL COMMENT 'ชื่อเล่ม', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + UNIQUE KEY ux_volume_project (project_id, volume_code) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "เล่ม" ของแบบคู่สัญญา'; + -- ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบคู่สัญญา CREATE TABLE contract_drawing_cats ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - project_id INT NOT NULL COMMENT 'โครงการ', - cat_code VARCHAR(50) NOT NULL COMMENT 'รหัสหมวดหมู่หลัก', - cat_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่หลัก', - description TEXT COMMENT 'คำอธิบาย', - sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - UNIQUE KEY ux_cat_project (project_id, cat_code) + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + cat_code VARCHAR(50) NOT NULL COMMENT 'รหัสหมวดหมู่หลัก', + cat_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่หลัก', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + UNIQUE KEY ux_cat_project (project_id, cat_code) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบคู่สัญญา'; + -- ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบคู่สัญญา CREATE TABLE contract_drawing_sub_cats ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - project_id INT NOT NULL COMMENT 'โครงการ', - sub_cat_code VARCHAR(50) NOT NULL COMMENT 'รหัสหมวดหมู่ย่อย', - sub_cat_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่ย่อย', - description TEXT COMMENT 'คำอธิบาย', - sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - UNIQUE KEY ux_subcat_project (project_id, sub_cat_code) + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + sub_cat_code VARCHAR(50) NOT NULL COMMENT 'รหัสหมวดหมู่ย่อย', + sub_cat_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่ย่อย', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + UNIQUE KEY ux_subcat_project (project_id, sub_cat_code) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบคู่สัญญา'; + -- ตารางเชื่อมระหว่าง หมวดหมู่หลัก-ย่อย (M:N) CREATE TABLE contract_drawing_subcat_cat_maps ( - project_id INT COMMENT 'ID ของโครงการ', - sub_cat_id INT COMMENT 'ID ของหมวดหมู่ย่อย', - cat_id INT COMMENT 'ID ของหมวดหมู่หลัก', - PRIMARY KEY ( - project_id, - sub_cat_id, - cat_id - ), - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - FOREIGN KEY (sub_cat_id) REFERENCES contract_drawing_sub_cats (id) ON DELETE CASCADE, - FOREIGN KEY (cat_id) REFERENCES contract_drawing_cats (id) ON DELETE CASCADE + project_id INT COMMENT 'ID ของโครงการ', + sub_cat_id INT COMMENT 'ID ของหมวดหมู่ย่อย', + cat_id INT COMMENT 'ID ของหมวดหมู่หลัก', + PRIMARY KEY ( + project_id, + sub_cat_id, + cat_id + ), + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (sub_cat_id) REFERENCES contract_drawing_sub_cats (id) ON DELETE CASCADE, + FOREIGN KEY (cat_id) REFERENCES contract_drawing_cats (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง หมวดหมู่หลัก - ย่อย (M :N)'; + -- ตาราง Master เก็บข้อมูล "แบบคู่สัญญา" CREATE TABLE contract_drawings ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - project_id INT NOT NULL COMMENT 'โครงการ', - condwg_no VARCHAR(255) NOT NULL COMMENT 'เลขที่แบบสัญญา', - title VARCHAR(255) NOT NULL COMMENT 'ชื่อแบบสัญญา', - sub_cat_id INT COMMENT 'หมวดหมู่ย่อย', - volume_id INT COMMENT 'เล่ม', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - deleted_at DATETIME NULL COMMENT 'วันที่ลบ', - updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - FOREIGN KEY (sub_cat_id) REFERENCES contract_drawing_sub_cats (id) ON DELETE RESTRICT, - FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes (id) ON DELETE RESTRICT, - UNIQUE KEY ux_condwg_no_project (project_id, condwg_no) + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + condwg_no VARCHAR(255) NOT NULL COMMENT 'เลขที่แบบสัญญา', + title VARCHAR(255) NOT NULL COMMENT 'ชื่อแบบสัญญา', + sub_cat_id INT COMMENT 'หมวดหมู่ย่อย', + volume_id INT COMMENT 'เล่ม', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (sub_cat_id) REFERENCES contract_drawing_sub_cats (id) ON DELETE RESTRICT, + FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes (id) ON DELETE RESTRICT, + UNIQUE KEY ux_condwg_no_project (project_id, condwg_no) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบคู่สัญญา"'; + -- ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบก่อสร้าง CREATE TABLE shop_drawing_main_categories ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - main_category_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสหมวดหมู่หลัก (เช่น ARCH, STR)', - main_category_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่หลัก', - description TEXT COMMENT 'คำอธิบาย', - sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด ' + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + main_category_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสหมวดหมู่หลัก (เช่น ARCH, STR)', + main_category_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่หลัก', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด ' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบก่อสร้าง'; + -- ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบก่อสร้าง CREATE TABLE shop_drawing_sub_categories ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - sub_category_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสหมวดหมู่ย่อย (เช่น STR - COLUMN)', - sub_category_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่ย่อย', - main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก', - description TEXT COMMENT 'คำอธิบาย', - sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id) + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + sub_category_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสหมวดหมู่ย่อย (เช่น STR - COLUMN)', + sub_category_name VARCHAR(255) NOT NULL COMMENT 'ชื่อหมวดหมู่ย่อย', + main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก', + description TEXT COMMENT 'คำอธิบาย', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master สำหรับ "หมวดหมู่ย่อย" ของแบบก่อสร้าง'; + -- ตาราง Master เก็บข้อมูล "แบบก่อสร้าง" CREATE TABLE shop_drawings ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - project_id INT NOT NULL COMMENT 'โครงการ', - drawing_number VARCHAR(100) NOT NULL UNIQUE COMMENT 'เลขที่ Shop Drawing', - title VARCHAR(500) NOT NULL COMMENT 'ชื่อแบบ', - main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก', - sub_category_id INT NOT NULL COMMENT 'หมวดหมู่ย่อย', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - deleted_at DATETIME NULL COMMENT 'วันที่ลบ', - updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', - FOREIGN KEY (project_id) REFERENCES projects (id), - FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id), - FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories (id) + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + drawing_number VARCHAR(100) NOT NULL UNIQUE COMMENT 'เลขที่ Shop Drawing', + title VARCHAR(500) NOT NULL COMMENT 'ชื่อแบบ', + main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก', + sub_category_id INT NOT NULL COMMENT 'หมวดหมู่ย่อย', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + deleted_at DATETIME NULL COMMENT 'วันที่ลบ', + updated_by INT COMMENT 'ผู้แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id), + FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id), + FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบก่อสร้าง"'; + -- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1:N) CREATE TABLE shop_drawing_revisions ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', - shop_drawing_id INT NOT NULL COMMENT 'Master ID', - revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)', - revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)', - revision_date DATE COMMENT 'วันที่ของ Revision', - description TEXT COMMENT 'คำอธิบายการแก้ไข', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - FOREIGN KEY (shop_drawing_id) REFERENCES shop_drawings (id) ON DELETE CASCADE, - UNIQUE KEY ux_sd_rev_drawing_revision ( - shop_drawing_id, - revision_number - ) + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision', + shop_drawing_id INT NOT NULL COMMENT 'Master ID', + revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)', + revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)', + revision_date DATE COMMENT 'วันที่ของ Revision', + description TEXT COMMENT 'คำอธิบายการแก้ไข', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + FOREIGN KEY (shop_drawing_id) REFERENCES shop_drawings (id) ON DELETE CASCADE, + UNIQUE KEY ux_sd_rev_drawing_revision (shop_drawing_id, revision_number) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1 :N)'; + -- ตารางเชื่อมระหว่าง shop_drawing_revisions กับ contract_drawings (M:N) CREATE TABLE shop_drawing_revision_contract_refs ( - shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', - contract_drawing_id INT COMMENT 'ID ของ Contract Drawing', - PRIMARY KEY ( - shop_drawing_revision_id, - contract_drawing_id - ), - FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions (id) ON DELETE CASCADE, - FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings (id) ON DELETE CASCADE + shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', + contract_drawing_id INT COMMENT 'ID ของ Contract Drawing', + PRIMARY KEY ( + shop_drawing_revision_id, + contract_drawing_id + ), + FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions (id) ON DELETE CASCADE, + FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง shop_drawing_revisions กับ contract_drawings (M :N)'; + -- ===================================================== -- 6. 🔄 Circulations (ใบเวียนภายใน) -- ===================================================== -- ตาราง Master เก็บสถานะใบเวียน CREATE TABLE circulation_status_codes ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสสถานะการดำเนินงาน', - description VARCHAR(50) NOT NULL COMMENT 'คำอธิบายสถานะการดำเนินงาน', - sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', - is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสสถานะการดำเนินงาน', + description VARCHAR(50) NOT NULL COMMENT 'คำอธิบายสถานะการดำเนินงาน', + sort_order INT DEFAULT 0 COMMENT 'ลำดับการแสดงผล', + is_active TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน ' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บสถานะใบเวียน'; + -- ตาราง "แม่" ของใบเวียนเอกสารภายใน CREATE TABLE circulations ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตารางใบเวียน', - correspondence_id INT UNIQUE COMMENT 'ID ของเอกสาร (จากตาราง correspondences)', - organization_id INT NOT NULL COMMENT 'ID ขององค์กรณ์ที่เป็นเจ้าของใบเวียนนี้', - circulation_no VARCHAR(100) NOT NULL COMMENT 'เลขที่ใบเวียน', - circulation_subject VARCHAR(500) NOT NULL COMMENT 'เรื่องใบเวียน', - circulation_status_code VARCHAR(20) NOT NULL COMMENT 'รหัสสถานะใบเวียน', - created_by_user_id INT NOT NULL COMMENT 'ID ของผู้สร้างใบเวียน', - submitted_at TIMESTAMP NULL COMMENT 'วันที่ส่งใบเวียน', - closed_at TIMESTAMP NULL COMMENT 'วันที่ปิดใบเวียน', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - FOREIGN KEY (correspondence_id) REFERENCES correspondences (id), - FOREIGN KEY (organization_id) REFERENCES organizations (id), - FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes (code), - FOREIGN KEY (created_by_user_id) REFERENCES users (user_id) + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตารางใบเวียน', + correspondence_id INT UNIQUE COMMENT 'ID ของเอกสาร (จากตาราง correspondences)', + organization_id INT NOT NULL COMMENT 'ID ขององค์กรณ์ที่เป็นเจ้าของใบเวียนนี้', + circulation_no VARCHAR(100) NOT NULL COMMENT 'เลขที่ใบเวียน', + circulation_subject VARCHAR(500) NOT NULL COMMENT 'เรื่องใบเวียน', + circulation_status_code VARCHAR(20) NOT NULL COMMENT 'รหัสสถานะใบเวียน', + created_by_user_id INT NOT NULL COMMENT 'ID ของผู้สร้างใบเวียน', + submitted_at TIMESTAMP NULL COMMENT 'วันที่ส่งใบเวียน', + closed_at TIMESTAMP NULL COMMENT 'วันที่ปิดใบเวียน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id), + FOREIGN KEY (organization_id) REFERENCES organizations (id), + FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes (code), + FOREIGN KEY (created_by_user_id) REFERENCES users (user_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของใบเวียนเอกสารภายใน'; + -- ===================================================== -- 7. 📤 Transmittals (เอกสารนำส่ง) -- ===================================================== -- ตารางข้อมูลเฉพาะของเอกสารนำส่ง (เป็นตารางลูก 1:1 ของ correspondences) CREATE TABLE transmittals ( - correspondence_id INT PRIMARY KEY COMMENT 'ID ของเอกสาร', - purpose ENUM( - 'FOR_APPROVAL', - 'FOR_INFORMATION', - 'FOR_REVIEW', - 'OTHER ' - ) COMMENT 'วัตถุประสงค์', - remarks TEXT COMMENT 'หมายเหตุ', - FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE + correspondence_id INT PRIMARY KEY COMMENT 'ID ของเอกสาร', + purpose ENUM( + 'FOR_APPROVAL', + 'FOR_INFORMATION', + 'FOR_REVIEW', + 'OTHER ' + ) COMMENT 'วัตถุประสงค์', + remarks TEXT COMMENT 'หมายเหตุ', + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางข้อมูลเฉพาะของเอกสารนำส่ง (เป็นตารางลูก 1 :1 ของ correspondences)'; + -- ตารางเชื่อมระหว่าง transmittals และเอกสารที่นำส่ง (M:N) CREATE TABLE transmittal_items ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของรายการ', - transmittal_id INT NOT NULL COMMENT 'ID ของ Transmittal', - item_correspondence_id INT NOT NULL COMMENT 'ID ของเอกสารที่แนบไป', - quantity INT DEFAULT 1 COMMENT 'จำนวน', - remarks VARCHAR(255) COMMENT 'หมายเหตุสำหรับรายการนี้', - FOREIGN KEY (transmittal_id) REFERENCES transmittals (correspondence_id) ON DELETE CASCADE, - FOREIGN KEY (item_correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, - UNIQUE KEY ux_transmittal_item ( - transmittal_id, - item_correspondence_id - ) + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของรายการ', + transmittal_id INT NOT NULL COMMENT 'ID ของ Transmittal', + item_correspondence_id INT NOT NULL COMMENT 'ID ของเอกสารที่แนบไป', + quantity INT DEFAULT 1 COMMENT 'จำนวน', + remarks VARCHAR(255) COMMENT 'หมายเหตุสำหรับรายการนี้', + FOREIGN KEY (transmittal_id) REFERENCES transmittals (correspondence_id) ON DELETE CASCADE, + FOREIGN KEY (item_correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + UNIQUE KEY ux_transmittal_item ( + transmittal_id, + item_correspondence_id + ) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อมระหว่าง transmittals และเอกสารที่นำส่ง (M :N)'; + -- ===================================================== -- 8. 📎 File Management (ไฟล์แนบ) -- ===================================================== @@ -774,96 +840,99 @@ CREATE TABLE transmittal_items ( -- รองรับ: Backend Plan T2.2, Req 3.9.1 -- เหตุผล: จัดการไฟล์ขยะ (Orphan Files) และตรวจสอบความถูกต้องไฟล์ CREATE TABLE attachments ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของไฟล์แนบ', - original_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ดั้งเดิมตอนอัปโหลด', - stored_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ที่เก็บจริงบน Server (ป้องกันชื่อซ้ำ)', - file_path VARCHAR(500) NOT NULL COMMENT 'Path ที่เก็บไฟล์ (บน QNAP / share / dms - data /)', - mime_type VARCHAR(100) NOT NULL COMMENT 'ประเภทไฟล์ (เช่น application / pdf)', - file_size INT NOT NULL COMMENT 'ขนาดไฟล์ (bytes)', - is_temporary BOOLEAN DEFAULT TRUE COMMENT 'True = ยังไม่ Commit ลง DB จริง', - temp_id VARCHAR(100) NULL COMMENT 'ID ชั่วคราวสำหรับอ้างอิงตอน Upload Phase 1', - uploaded_by_user_id INT NOT NULL COMMENT 'ผู้อัปโหลดไฟล์', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่อัปโหลด', - expires_at DATETIME NULL COMMENT 'เวลาหมดอายุของไฟล์ Temp', - CHECKSUM VARCHAR(64) NULL COMMENT 'SHA -256 Checksum', - FOREIGN KEY (uploaded_by_user_id) REFERENCES users (user_id) ON DELETE CASCADE + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของไฟล์แนบ', + original_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ดั้งเดิมตอนอัปโหลด', + stored_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ที่เก็บจริงบน Server (ป้องกันชื่อซ้ำ)', + file_path VARCHAR(500) NOT NULL COMMENT 'Path ที่เก็บไฟล์ (บน QNAP / share / dms - data /)', + mime_type VARCHAR(100) NOT NULL COMMENT 'ประเภทไฟล์ (เช่น application / pdf)', + file_size INT NOT NULL COMMENT 'ขนาดไฟล์ (bytes)', + is_temporary BOOLEAN DEFAULT TRUE COMMENT 'True = ยังไม่ Commit ลง DB จริง', + temp_id VARCHAR(100) NULL COMMENT 'ID ชั่วคราวสำหรับอ้างอิงตอน Upload Phase 1', + uploaded_by_user_id INT NOT NULL COMMENT 'ผู้อัปโหลดไฟล์', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่อัปโหลด', + expires_at DATETIME NULL COMMENT 'เวลาหมดอายุของไฟล์ Temp', + CHECKSUM VARCHAR(64) NULL COMMENT 'SHA -256 Checksum', + FOREIGN KEY (uploaded_by_user_id) REFERENCES users (user_id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "กลาง" เก็บไฟล์แนบทั้งหมดของระบบ'; + -- ตารางเชื่อม correspondences กับ attachments (M:N) CREATE TABLE correspondence_attachments ( - correspondence_id INT COMMENT 'ID ของเอกสาร', - attachment_id INT COMMENT 'ID ของไฟล์แนบ', - is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', - PRIMARY KEY ( - correspondence_id, - attachment_id - ), - FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, - FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE + correspondence_id INT COMMENT 'ID ของเอกสาร', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', + PRIMARY KEY (correspondence_id, attachment_id), + FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม correspondences กับ attachments (M :N)'; + -- ตารางเชื่อม circulations กับ attachments (M:N) CREATE TABLE circulation_attachments ( - circulation_id INT COMMENT 'ID ของใบเวียน', - attachment_id INT COMMENT 'ID ของไฟล์แนบ', - is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลักของใบเวียน)', - PRIMARY KEY (circulation_id, attachment_id), - FOREIGN KEY (circulation_id) REFERENCES circulations (id) ON DELETE CASCADE, - FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE + circulation_id INT COMMENT 'ID ของใบเวียน', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลักของใบเวียน)', + PRIMARY KEY (circulation_id, attachment_id), + FOREIGN KEY (circulation_id) REFERENCES circulations (id) ON DELETE CASCADE, + FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม circulations กับ attachments (M :N)'; + -- ตารางเชื่อม shop_drawing_revisions กับ attachments (M:N) CREATE TABLE shop_drawing_revision_attachments ( - shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', - attachment_id INT COMMENT 'ID ของไฟล์แนบ', - file_type ENUM( - 'PDF', - 'DWG', - 'SOURCE', - 'OTHER ' - ) COMMENT 'ประเภทไฟล์', - is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', - PRIMARY KEY ( - shop_drawing_revision_id, - attachment_id - ), - FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions (id) ON DELETE CASCADE, - FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE + shop_drawing_revision_id INT COMMENT 'ID ของ Shop Drawing Revision', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + file_type ENUM( + 'PDF', + 'DWG', + 'SOURCE', + 'OTHER ' + ) COMMENT 'ประเภทไฟล์', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', + PRIMARY KEY ( + shop_drawing_revision_id, + attachment_id + ), + FOREIGN KEY (shop_drawing_revision_id) REFERENCES shop_drawing_revisions (id) ON DELETE CASCADE, + FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม shop_drawing_revisions กับ attachments (M :N)'; + -- ตารางเชื่อม contract_drawings กับ attachments (M:N) CREATE TABLE contract_drawing_attachments ( - contract_drawing_id INT COMMENT 'ID ของ Contract Drawing', - attachment_id INT COMMENT 'ID ของไฟล์แนบ', - file_type ENUM( - 'PDF', - 'DWG', - 'SOURCE', - 'OTHER ' - ) COMMENT 'ประเภทไฟล์', - is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', - PRIMARY KEY ( - contract_drawing_id, - attachment_id - ), - FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings (id) ON DELETE CASCADE, - FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE + contract_drawing_id INT COMMENT 'ID ของ Contract Drawing', + attachment_id INT COMMENT 'ID ของไฟล์แนบ', + file_type ENUM( + 'PDF', + 'DWG', + 'SOURCE', + 'OTHER ' + ) COMMENT 'ประเภทไฟล์', + is_main_document BOOLEAN DEFAULT FALSE COMMENT '(1 = ไฟล์หลัก)', + PRIMARY KEY ( + contract_drawing_id, + attachment_id + ), + FOREIGN KEY (contract_drawing_id) REFERENCES contract_drawings (id) ON DELETE CASCADE, + FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเชื่อม contract_drawings กับ attachments (M :N)'; + -- ===================================================== -- 9. 🔢 Document Numbering (การสร้างเลขที่เอกสาร) -- ===================================================== -- ตาราง Master เก็บ "รูปแบบ" Template ของเลขที่เอกสาร CREATE TABLE document_number_formats ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', - project_id INT NOT NULL COMMENT 'โครงการ', - correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร', - format_template VARCHAR(255) NOT NULL COMMENT 'รูปแบบ Template (เช่น { ORG_CODE } - { TYPE_CODE } - { SEQ :4 })', - description TEXT COMMENT 'คำอธิบายรูปแบบนี้', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE CASCADE, - UNIQUE KEY uk_project_type ( - project_id, - correspondence_type_id - ) + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง', + project_id INT NOT NULL COMMENT 'โครงการ', + correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร', + format_template VARCHAR(255) NOT NULL COMMENT 'รูปแบบ Template (เช่น { ORG_CODE } - { TYPE_CODE } - { SEQ :4 })', + description TEXT COMMENT 'คำอธิบายรูปแบบนี้', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE CASCADE, + UNIQUE KEY uk_project_type ( + project_id, + correspondence_type_id + ) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บ "รูปแบบ" Template ของเลขที่เอกสาร'; + -- ========================================================== -- [v1.5.1 UPDATE] ตารางเก็บ "ตัวนับ" (Running Number) ล่าสุด -- เปลี่ยนแปลงหลัก: @@ -875,54 +944,66 @@ CREATE TABLE document_number_formats ( -- รองรับ: Backend Plan T2.3, Req 3.11.5, specs v1.5.1 -- ========================================================== CREATE TABLE document_number_counters ( - -- [v1.5.1] Composite Primary Key Columns (8 columns total) - project_id INT NOT NULL COMMENT 'โครงการ', - originator_organization_id INT NOT NULL COMMENT 'องค์กรผู้ส่ง', - recipient_organization_id INT NULL COMMENT '[v1.5.1 NEW] องค์กรผู้รับ (NULL = ทุกองค์กร)', - correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.)', - sub_type_id INT DEFAULT 0 COMMENT '[v1.5.1 NEW] ประเภทย่อย สำหรับ TRANSMITTAL (0 = ไม่ระบุ)', - rfa_type_id INT DEFAULT 0 COMMENT '[v1.5.1 NEW] ประเภท RFA เช่น SHD, RPT, MAT (0 = ไม่ใช่ RFA)', - discipline_id INT DEFAULT 0 COMMENT 'สาขางาน เช่น TER, STR, GEO (0 = ไม่ระบุ)', - current_year INT NOT NULL COMMENT 'ปี ค.ศ. ของตัวนับ (auto-reset ทุกปี)', - -- Counter Data - version INT DEFAULT 0 NOT NULL COMMENT 'Optimistic Lock Version (TypeORM @VersionColumn)', - last_number INT DEFAULT 0 COMMENT 'เลขที่ล่าสุดที่ใช้ไปแล้ว (auto-increment)', - -- [v1.5.1 UPDATE] Primary Key: 5 columns -> 8 columns - -- ใช้ COALESCE เพื่อรองรับ NULL ใน recipient_organization_id - PRIMARY KEY ( - project_id, - originator_organization_id, - COALESCE(recipient_organization_id, 0), - -- [v1.5.1 NEW] Handle NULL values - correspondence_type_id, - sub_type_id, - -- [v1.5.1 NEW] - rfa_type_id, - -- [v1.5.1 NEW] - discipline_id, - current_year - ), - -- Foreign Keys - FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, - FOREIGN KEY (originator_organization_id) REFERENCES organizations (id) ON DELETE CASCADE, - FOREIGN KEY (recipient_organization_id) REFERENCES organizations (id) ON DELETE CASCADE, - FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE CASCADE, - -- [v1.5.1 NEW] Performance Indexes - INDEX idx_counter_lookup ( - project_id, - correspondence_type_id, - current_year - ), - INDEX idx_counter_org ( - originator_organization_id, - current_year - ), - -- [v1.5.1 NEW] Data Validation Constraints - CONSTRAINT chk_last_number_positive CHECK (last_number >= 0), - CONSTRAINT chk_current_year_valid CHECK ( - current_year BETWEEN 2020 AND 2100 - ) + -- [v1.5.1] Composite Primary Key Columns (8 columns total) + project_id INT NOT NULL COMMENT 'โครงการ', + originator_organization_id INT NOT NULL COMMENT 'องค์กรผู้ส่ง', + -- เปลี่ยนจาก NULL เป็น DEFAULT -1 + recipient_organization_id INT NOT NULL DEFAULT -1 COMMENT '[v1.5.1 NEW] องค์กรผู้รับ (-1 = ทุกองค์กร)', + -- recipient_organization_id INT NULL COMMENT '[v1.5.1 NEW] องค์กรผู้รับ (NULL = ทุกองค์กร)', + correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร (LETTER, RFA, TRANSMITTAL, etc.)', + sub_type_id INT DEFAULT 0 COMMENT '[v1.5.1 NEW] ประเภทย่อย สำหรับ TRANSMITTAL (0 = ไม่ระบุ)', + rfa_type_id INT DEFAULT 0 COMMENT '[v1.5.1 NEW] ประเภท RFA เช่น SHD, RPT, MAT (0 = ไม่ใช่ RFA)', + discipline_id INT DEFAULT 0 COMMENT 'สาขางาน เช่น TER, STR, GEO (0 = ไม่ระบุ)', + current_year INT NOT NULL COMMENT 'ปี ค.ศ. ของตัวนับ (auto-reset ทุกปี)', + -- Counter Data + version INT DEFAULT 0 NOT NULL COMMENT 'Optimistic Lock Version (TypeORM @VersionColumn)', + last_number INT DEFAULT 0 COMMENT 'เลขที่ล่าสุดที่ใช้ไปแล้ว (auto-increment)', + -- [v1.5.1 UPDATE] Primary Key: 5 columns -> 8 columns + -- ใช้ COALESCE เพื่อรองรับ NULL ใน recipient_organization_id + PRIMARY KEY ( + project_id, + originator_organization_id, + recipient_organization_id, + -- [v1.5.1 NEW] Handle NULL values + correspondence_type_id, + sub_type_id, + -- [v1.5.1 NEW] + rfa_type_id, + -- [v1.5.1 NEW] + discipline_id, + current_year + ), + -- Foreign Keys + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (originator_organization_id) REFERENCES organizations (id) ON DELETE CASCADE, + FOREIGN KEY (recipient_organization_id) REFERENCES organizations (id) ON DELETE CASCADE, + FOREIGN KEY (correspondence_type_id) REFERENCES correspondence_types (id) ON DELETE CASCADE, + -- ใช้ CHECK constraint เพื่อไม่ให้ตรวจสอบเมื่อเป็น -1 + CONSTRAINT fk_recipient_when_not_all FOREIGN KEY (recipient_organization_id) REFERENCES organizations (id) ON DELETE CASCADE, + -- [v1.5.1 NEW] Performance Indexes + INDEX idx_counter_lookup ( + project_id, + correspondence_type_id, + current_year + ), + INDEX idx_counter_org ( + originator_organization_id, + current_year + ), + -- [v1.5.1 NEW] Data Validation Constraints + -- CONSTRAINT chk_last_number_positive CHECK (last_number >= 0), + -- CONSTRAINT chk_current_year_valid CHECK (current_year BETWEEN 2020 AND 2100) + -- Constraints + CONSTRAINT chk_last_number_positive CHECK (last_number >= 0), + CONSTRAINT chk_current_year CHECK ( + current_year BETWEEN 2020 AND 2100 + ), + CONSTRAINT chk_recipient_special CHECK ( + recipient_organization_id = -1 + OR recipient_organization_id > 0 + ) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '[v1.5.1 UPDATE] ตารางเก็บ Running Number Counters - รองรับ 8-column composite PK'; + -- ========================================================== -- [v1.5.1 NEW] ตารางเก็บ Audit Trail สำหรับการสร้างเลขที่เอกสาร -- เพิ่มตาราง: document_number_audit @@ -930,31 +1011,32 @@ CREATE TABLE document_number_counters ( -- รองรับ: Req 3.11.8, Backend Plan T2.8 -- ========================================================== CREATE TABLE document_number_audit ( - id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID ของ audit record', - -- Document Info - document_id INT NOT NULL COMMENT 'ID ของเอกสารที่สร้างเลขที่ (correspondences.id)', - generated_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสารที่สร้าง (ผลลัพธ์)', - counter_key JSON NOT NULL COMMENT 'Counter key ที่ใช้ (JSON format) - 8 fields', - template_used VARCHAR(200) NOT NULL COMMENT 'Template ที่ใช้ในการสร้าง', - -- User Info - user_id INT NOT NULL COMMENT 'ผู้ขอสร้างเลขที่', - ip_address VARCHAR(45) COMMENT 'IP address ของผู้ขอ (IPv4/IPv6)', - user_agent TEXT COMMENT 'User agent string (browser info)', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่/เวลาที่สร้าง', - -- Performance & Error Tracking - retry_count INT DEFAULT 0 COMMENT 'จำนวนครั้งที่ retry ก่อนสำเร็จ', - lock_wait_ms INT COMMENT 'เวลารอ Redis lock (milliseconds)', - total_duration_ms INT COMMENT 'เวลารวมทั้งหมดในการสร้าง (milliseconds)', - fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE' COMMENT 'Fallback strategy ที่ถูกใช้ (NONE=normal, DB_LOCK=Redis down, RETRY=conflict)', - -- Indexes for performance - INDEX idx_document_id (document_id), - INDEX idx_user_id (user_id), - INDEX idx_created_at (created_at), - INDEX idx_generated_number (generated_number), - -- Foreign Keys - FOREIGN KEY (document_id) REFERENCES correspondences (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users (user_id) + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID ของ audit record', + -- Document Info + document_id INT NOT NULL COMMENT 'ID ของเอกสารที่สร้างเลขที่ (correspondences.id)', + generated_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสารที่สร้าง (ผลลัพธ์)', + counter_key JSON NOT NULL COMMENT 'Counter key ที่ใช้ (JSON format) - 8 fields', + template_used VARCHAR(200) NOT NULL COMMENT 'Template ที่ใช้ในการสร้าง', + -- User Info + user_id INT NOT NULL COMMENT 'ผู้ขอสร้างเลขที่', + ip_address VARCHAR(45) COMMENT 'IP address ของผู้ขอ (IPv4/IPv6)', + user_agent TEXT COMMENT 'User agent string (browser info)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่/เวลาที่สร้าง', + -- Performance & Error Tracking + retry_count INT DEFAULT 0 COMMENT 'จำนวนครั้งที่ retry ก่อนสำเร็จ', + lock_wait_ms INT COMMENT 'เวลารอ Redis lock (milliseconds)', + total_duration_ms INT COMMENT 'เวลารวมทั้งหมดในการสร้าง (milliseconds)', + fallback_used ENUM('NONE', 'DB_LOCK', 'RETRY') DEFAULT 'NONE' COMMENT 'Fallback strategy ที่ถูกใช้ (NONE=normal, DB_LOCK=Redis down, RETRY=conflict)', + -- Indexes for performance + INDEX idx_document_id (document_id), + INDEX idx_user_id (user_id), + INDEX idx_created_at (created_at), + INDEX idx_generated_number (generated_number), + -- Foreign Keys + FOREIGN KEY (document_id) REFERENCES correspondences (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (user_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '[v1.5.1 NEW] Audit Trail สำหรับการสร้างเลขที่เอกสาร - เก็บ ≥ 7 ปี'; + -- ========================================================== -- [v1.5.1 NEW] ตารางเก็บ Error Logs สำหรับ Document Numbering -- เพิ่มตาราง: document_number_errors @@ -962,35 +1044,36 @@ CREATE TABLE document_number_audit ( -- รองรับ: Req 3.11.6, Ops monitoring requirements -- ========================================================== CREATE TABLE document_number_errors ( - id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID ของ error record', - -- Error Classification - error_type ENUM( - 'LOCK_TIMEOUT', - -- Redis lock timeout - 'VERSION_CONFLICT', - -- Optimistic lock version mismatch - 'DB_ERROR', - -- Database connection/query error - 'REDIS_ERROR', - -- Redis connection error - 'VALIDATION_ERROR' -- Template/input validation error - ) NOT NULL COMMENT 'ประเภท error (5 types)', - -- Error Details - error_message TEXT COMMENT 'ข้อความ error (stack top)', - stack_trace TEXT COMMENT 'Stack trace แบบเต็ม (สำหรับ debugging)', - context_data JSON COMMENT 'Context ของ request (user, project, counter_key, etc.)', - -- User Info - user_id INT COMMENT 'ผู้ที่เกิด error', - ip_address VARCHAR(45) COMMENT 'IP address', - -- Timestamps - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่เกิด error', - resolved_at TIMESTAMP NULL COMMENT 'วันที่แก้ไขแล้ว (NULL = ยังไม่แก้)', - -- Indexes for troubleshooting - INDEX idx_error_type (error_type), - INDEX idx_created_at (created_at), - INDEX idx_user_id (user_id), - INDEX idx_unresolved (resolved_at) -- Find unresolved errors + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID ของ error record', + -- Error Classification + error_type ENUM( + 'LOCK_TIMEOUT', + -- Redis lock timeout + 'VERSION_CONFLICT', + -- Optimistic lock version mismatch + 'DB_ERROR', + -- Database connection/query error + 'REDIS_ERROR', + -- Redis connection error + 'VALIDATION_ERROR' -- Template/input validation error + ) NOT NULL COMMENT 'ประเภท error (5 types)', + -- Error Details + error_message TEXT COMMENT 'ข้อความ error (stack top)', + stack_trace TEXT COMMENT 'Stack trace แบบเต็ม (สำหรับ debugging)', + context_data JSON COMMENT 'Context ของ request (user, project, counter_key, etc.)', + -- User Info + user_id INT COMMENT 'ผู้ที่เกิด error', + ip_address VARCHAR(45) COMMENT 'IP address', + -- Timestamps + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่เกิด error', + resolved_at TIMESTAMP NULL COMMENT 'วันที่แก้ไขแล้ว (NULL = ยังไม่แก้)', + -- Indexes for troubleshooting + INDEX idx_error_type (error_type), + INDEX idx_created_at (created_at), + INDEX idx_user_id (user_id), + INDEX idx_unresolved (resolved_at) -- Find unresolved errors ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '[v1.5.1 NEW] Error Log สำหรับ Document Numbering System'; + -- ===================================================== -- 10. ⚙️ System & Logs (ระบบและ Log) -- ===================================================== @@ -998,168 +1081,154 @@ CREATE TABLE document_number_errors ( -- รองรับ: Backend Plan T2.5.1, Req 6.11.1 -- เหตุผล: เพื่อ Validate โครงสร้าง JSON Details ของเอกสารแต่ละประเภทแบบ Centralized CREATE TABLE json_schemas ( - id INT AUTO_INCREMENT PRIMARY KEY, - schema_code VARCHAR(100) NOT NULL COMMENT 'รหัส Schema (เช่น RFA_DWG)', - version INT NOT NULL DEFAULT 1 COMMENT 'เวอร์ชันของ Schema', - table_name VARCHAR(100) NOT NULL COMMENT 'ชื่อตารางเป้าหมาย (เช่น rfa_revisions)', - schema_definition JSON NOT NULL COMMENT 'โครงสร้าง Data Schema (AJV Standard)', - ui_schema JSON NULL COMMENT 'โครงสร้าง UI Schema สำหรับ Frontend', - virtual_columns JSON NULL COMMENT 'Config สำหรับสร้าง Virtual Columns', - migration_script JSON NULL COMMENT 'Script สำหรับแปลงข้อมูลจากเวอร์ชันก่อนหน้า', - is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - -- ป้องกัน Schema Code ซ้ำกันใน Version เดียวกัน - UNIQUE KEY uk_schema_version (schema_code, version) + id INT AUTO_INCREMENT PRIMARY KEY, + schema_code VARCHAR(100) NOT NULL COMMENT 'รหัส Schema (เช่น RFA_DWG)', + version INT NOT NULL DEFAULT 1 COMMENT 'เวอร์ชันของ Schema', + table_name VARCHAR(100) NOT NULL COMMENT 'ชื่อตารางเป้าหมาย (เช่น rfa_revisions)', + schema_definition JSON NOT NULL COMMENT 'โครงสร้าง Data Schema (AJV Standard)', + ui_schema JSON NULL COMMENT 'โครงสร้าง UI Schema สำหรับ Frontend', + virtual_columns JSON NULL COMMENT 'Config สำหรับสร้าง Virtual Columns', + migration_script JSON NULL COMMENT 'Script สำหรับแปลงข้อมูลจากเวอร์ชันก่อนหน้า', + is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + -- ป้องกัน Schema Code ซ้ำกันใน Version เดียวกัน + UNIQUE KEY uk_schema_version (schema_code, version) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางเก็บ JSON Schema และ Configuration'; + -- 1.2 User Preferences -- รองรับ: Req 5.5, 6.8.3 -- เหตุผล: แยกการตั้งค่า Notification และ UI ออกจากตาราง Users หลัก CREATE TABLE user_preferences ( - user_id INT PRIMARY KEY, - notify_email BOOLEAN DEFAULT TRUE, - notify_line BOOLEAN DEFAULT TRUE, - digest_mode BOOLEAN DEFAULT FALSE COMMENT 'รับแจ้งเตือนแบบรวม (Digest) แทน Real - time', - ui_theme VARCHAR(20) DEFAULT 'light', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT fk_user_prefs_user FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE + user_id INT PRIMARY KEY, + notify_email BOOLEAN DEFAULT TRUE, + notify_line BOOLEAN DEFAULT TRUE, + digest_mode BOOLEAN DEFAULT FALSE COMMENT 'รับแจ้งเตือนแบบรวม (Digest) แทน Real - time', + ui_theme VARCHAR(20) DEFAULT 'light', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_user_prefs_user FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + -- ตารางเก็บบันทึกการกระทำของผู้ใช้ -- 4.1 Audit Logs Enhancements -- รองรับ: Req 6.1 -- เหตุผล: รองรับ Distributed Tracing และระบุความรุนแรง CREATE TABLE audit_logs ( - audit_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID ของ Log', - request_id VARCHAR(100) NULL COMMENT 'Trace ID linking to app logs', - user_id INT COMMENT 'ผู้กระทำ', - ACTION VARCHAR(100) NOT NULL COMMENT 'การกระทำ ( + audit_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID ของ Log', + request_id VARCHAR(100) NULL COMMENT 'Trace ID linking to app logs', + user_id INT COMMENT 'ผู้กระทำ', + ACTION VARCHAR(100) NOT NULL COMMENT 'การกระทำ ( เช่น rfa.create, correspondence.update, login.success )', - severity ENUM( - 'INFO', - 'WARN', - 'ERROR', - 'CRITICAL ' - ) DEFAULT 'INFO', - entity_type VARCHAR(50) COMMENT 'ตาราง / โมดูล (เช่น ''rfa '', ''correspondence '')', - entity_id VARCHAR(50) COMMENT 'Primary ID ของระเบียนที่ได้รับผลกระทำ', - details_json JSON COMMENT 'ข้อมูลบริบท', - ip_address VARCHAR(45) COMMENT 'IP Address', - user_agent VARCHAR(255) COMMENT 'User Agent', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'เวลาที่กระทำ', - -- [แก้ไข] รวม created_at เข้ามาใน Primary Key เพื่อรองรับ Partition - PRIMARY KEY (audit_id, created_at), - -- [แก้ไข] ใช้ Index ธรรมดาแทน Foreign Key เพื่อไม่ให้ติดข้อจำกัดของ Partition Table - INDEX idx_audit_user (user_id), - INDEX idx_audit_action (ACTION), - INDEX idx_audit_entity (entity_type, entity_id), - INDEX idx_audit_created (created_at) + severity ENUM( + 'INFO', + 'WARN', + 'ERROR', + 'CRITICAL ' + ) DEFAULT 'INFO', + entity_type VARCHAR(50) COMMENT 'ตาราง / โมดูล (เช่น ''rfa '', ''correspondence '')', + entity_id VARCHAR(50) COMMENT 'Primary ID ของระเบียนที่ได้รับผลกระทำ', + details_json JSON COMMENT 'ข้อมูลบริบท', + ip_address VARCHAR(45) COMMENT 'IP Address', + user_agent VARCHAR(255) COMMENT 'User Agent', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'เวลาที่กระทำ', + -- [แก้ไข] รวม created_at เข้ามาใน Primary Key เพื่อรองรับ Partition + PRIMARY KEY (audit_id, created_at), + -- [แก้ไข] ใช้ Index ธรรมดาแทน Foreign Key เพื่อไม่ให้ติดข้อจำกัดของ Partition Table + INDEX idx_audit_user (user_id), + INDEX idx_audit_action (ACTION), + INDEX idx_audit_entity (entity_type, entity_id), + INDEX idx_audit_created (created_at) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บบันทึกการกระทำของผู้ใช้' -- [เพิ่ม] คำสั่ง Partition -PARTITION BY - RANGE (YEAR(created_at)) ( - PARTITION p_old - VALUES - LESS THAN (2024), - PARTITION p2024 - VALUES - LESS THAN (2025), - PARTITION p2025 - VALUES - LESS THAN (2026), - PARTITION p2026 - VALUES - LESS THAN (2027), - PARTITION p2027 - VALUES - LESS THAN (2028), - PARTITION p2028 - VALUES - LESS THAN (2029), - PARTITION p2029 - VALUES - LESS THAN (2030), - PARTITION p2030 - VALUES - LESS THAN (2031), - PARTITION p_future - VALUES - LESS THAN MAXVALUE - ); +PARTITION BY RANGE (YEAR(created_at)) ( + PARTITION p_old + VALUES LESS THAN (2024), + PARTITION p2024 + VALUES LESS THAN (2025), + PARTITION p2025 + VALUES LESS THAN (2026), + PARTITION p2026 + VALUES LESS THAN (2027), + PARTITION p2027 + VALUES LESS THAN (2028), + PARTITION p2028 + VALUES LESS THAN (2029), + PARTITION p2029 + VALUES LESS THAN (2030), + PARTITION p2030 + VALUES LESS THAN (2031), + PARTITION p_future + VALUES LESS THAN MAXVALUE +); + -- ตารางสำหรับจัดการการแจ้งเตือน (Email/Line/System) CREATE TABLE notifications ( - id INT NOT NULL AUTO_INCREMENT COMMENT 'ID ของการแจ้งเตือน', - user_id INT NOT NULL COMMENT 'ID ผู้ใช้', - title VARCHAR(255) NOT NULL COMMENT 'หัวข้อการแจ้งเตือน', - message TEXT NOT NULL COMMENT 'รายละเอียดการแจ้งเตือน', - notification_type ENUM('EMAIL', 'LINE', 'SYSTEM ') NOT NULL COMMENT 'ประเภท (EMAIL, LINE, SYSTEM)', - is_read BOOLEAN DEFAULT FALSE COMMENT 'สถานะการอ่าน', - entity_type VARCHAR(50) COMMENT 'เช่น ''rfa '', + id INT NOT NULL AUTO_INCREMENT COMMENT 'ID ของการแจ้งเตือน', + user_id INT NOT NULL COMMENT 'ID ผู้ใช้', + title VARCHAR(255) NOT NULL COMMENT 'หัวข้อการแจ้งเตือน', + message TEXT NOT NULL COMMENT 'รายละเอียดการแจ้งเตือน', + notification_type ENUM('EMAIL', 'LINE', 'SYSTEM ') NOT NULL COMMENT 'ประเภท (EMAIL, LINE, SYSTEM)', + is_read BOOLEAN DEFAULT FALSE COMMENT 'สถานะการอ่าน', + entity_type VARCHAR(50) COMMENT 'เช่น ''rfa '', ''circulation ''', - entity_id INT COMMENT 'ID ของเอนทิตีที่เกี่ยวข้อง', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - -- [แก้ไข] รวม created_at เข้ามาใน Primary Key - PRIMARY KEY (id, created_at), - -- [แก้ไข] ใช้ Index ธรรมดาแทน Foreign Key - INDEX idx_notif_user (user_id), - INDEX idx_notif_type (notification_type), - INDEX idx_notif_read (is_read), - INDEX idx_notif_created (created_at) + entity_id INT COMMENT 'ID ของเอนทิตีที่เกี่ยวข้อง', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + -- [แก้ไข] รวม created_at เข้ามาใน Primary Key + PRIMARY KEY (id, created_at), + -- [แก้ไข] ใช้ Index ธรรมดาแทน Foreign Key + INDEX idx_notif_user (user_id), + INDEX idx_notif_type (notification_type), + INDEX idx_notif_read (is_read), + INDEX idx_notif_created (created_at) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการการแจ้งเตือน (Email / Line / System)' -- [เพิ่ม] คำสั่ง Partition -PARTITION BY - RANGE (YEAR(created_at)) ( - PARTITION p_old - VALUES - LESS THAN (2024), - PARTITION p2024 - VALUES - LESS THAN (2025), - PARTITION p2025 - VALUES - LESS THAN (2026), - PARTITION p2026 - VALUES - LESS THAN (2027), - PARTITION p2027 - VALUES - LESS THAN (2028), - PARTITION p2028 - VALUES - LESS THAN (2029), - PARTITION p2029 - VALUES - LESS THAN (2030), - PARTITION p2030 - VALUES - LESS THAN (2031), - PARTITION p_future - VALUES - LESS THAN MAXVALUE - ); +PARTITION BY RANGE (YEAR(created_at)) ( + PARTITION p_old + VALUES LESS THAN (2024), + PARTITION p2024 + VALUES LESS THAN (2025), + PARTITION p2025 + VALUES LESS THAN (2026), + PARTITION p2026 + VALUES LESS THAN (2027), + PARTITION p2027 + VALUES LESS THAN (2028), + PARTITION p2028 + VALUES LESS THAN (2029), + PARTITION p2029 + VALUES LESS THAN (2030), + PARTITION p2030 + VALUES LESS THAN (2031), + PARTITION p_future + VALUES LESS THAN MAXVALUE +); + -- ตารางสำหรับจัดการดัชนีการค้นหาขั้นสูง (Full-text Search) CREATE TABLE search_indices ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของดัชนี', - entity_type VARCHAR(50) NOT NULL COMMENT 'ชนิดเอนทิตี (เช่น ''correspondence '', ''rfa '')', - entity_id INT NOT NULL COMMENT 'ID ของเอนทิตี', - content TEXT NOT NULL COMMENT 'เนื้อหาที่จะค้นหา', - indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง / อัปเดตัชนี ' + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของดัชนี', + entity_type VARCHAR(50) NOT NULL COMMENT 'ชนิดเอนทิตี (เช่น ''correspondence '', ''rfa '')', + entity_id INT NOT NULL COMMENT 'ID ของเอนทิตี', + content TEXT NOT NULL COMMENT 'เนื้อหาที่จะค้นหา', + indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง / อัปเดตัชนี ' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการดัชนีการค้นหาขั้นสูง (Full - text Search)'; + -- ตารางสำหรับบันทึกประวัติการสำรองข้อมูล CREATE TABLE backup_logs ( - id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของการสำรอง', - backup_type ENUM('DATABASE', 'FILES', 'FULL') NOT NULL COMMENT 'ประเภท (DATABASE, FILES, FULL)', - backup_path VARCHAR(500) NOT NULL COMMENT 'ตำแหน่งไฟล์สำรอง', - file_size BIGINT COMMENT 'ขนาดไฟล์', - STATUS ENUM( - 'STARTED', - 'COMPLETED', - 'FAILED' - ) NOT NULL COMMENT 'สถานะ', - started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'เวลาเริ่มต้น', - completed_at TIMESTAMP NULL COMMENT 'เวลาเสร็จสิ้น', - error_message TEXT COMMENT 'ข้อความผิดพลาด (ถ้ามี)' + id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของการสำรอง', + backup_type ENUM('DATABASE', 'FILES', 'FULL') NOT NULL COMMENT 'ประเภท (DATABASE, FILES, FULL)', + backup_path VARCHAR(500) NOT NULL COMMENT 'ตำแหน่งไฟล์สำรอง', + file_size BIGINT COMMENT 'ขนาดไฟล์', + STATUS ENUM( + 'STARTED', + 'COMPLETED', + 'FAILED' + ) NOT NULL COMMENT 'สถานะ', + started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'เวลาเริ่มต้น', + completed_at TIMESTAMP NULL COMMENT 'เวลาเสร็จสิ้น', + error_message TEXT COMMENT 'ข้อความผิดพลาด (ถ้ามี)' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับบันทึกประวัติการสำรองข้อมูล'; + -- 4.2 Virtual Columns for JSON Search (ตัวอย่างสำหรับ Correspondence) -- รองรับ: Backend Plan T2.1, Req 3.11.3 -- เหตุผล: เพิ่มความเร็วในการ Search/Sort ข้อมูลที่อยู่ใน JSON details @@ -1168,37 +1237,42 @@ CREATE TABLE backup_logs ( ALTER TABLE correspondence_revisions ADD COLUMN v_ref_project_id INT GENERATED ALWAYS AS ( JSON_UNQUOTE( - JSON_EXTRACT(details, '$.projectId') + JSON_EXTRACT(details, '$.projectId') ) -) VIRTUAL, -ADD INDEX idx_corr_rev_v_project (v_ref_project_id); + ) VIRTUAL, + ADD INDEX idx_corr_rev_v_project (v_ref_project_id); + -- ตัวอย่าง: ดึง Document Type ย่อยจาก details ALTER TABLE correspondence_revisions ADD COLUMN v_doc_subtype VARCHAR(50) GENERATED ALWAYS AS ( - JSON_UNQUOTE( - JSON_EXTRACT(details, '$.subType') - ) -) VIRTUAL, -ADD INDEX idx_corr_rev_v_subtype (v_doc_subtype); + JSON_UNQUOTE(JSON_EXTRACT(details, '$.subType')) + ) VIRTUAL, + ADD INDEX idx_corr_rev_v_subtype (v_doc_subtype); + -- 2. ปรับปรุงตาราง correspondence_revisions -- เพิ่ม Virtual Columns และ Schema Version ALTER TABLE correspondence_revisions -ADD COLUMN schema_version INT DEFAULT 1 COMMENT 'เวอร์ชันของ Schema ที่ใช้กับ details' AFTER details; +ADD COLUMN schema_version INT DEFAULT 1 COMMENT 'เวอร์ชันของ Schema ที่ใช้กับ details' +AFTER details; + -- ทำแบบเดียวกันกับ RFA Revisions หากมีการเก็บ JSON details ALTER TABLE rfa_revisions -ADD COLUMN details JSON NULL COMMENT 'RFA Specific Details' AFTER description; +ADD COLUMN details JSON NULL COMMENT 'RFA Specific Details' +AFTER description; ALTER TABLE rfa_revisions ADD COLUMN v_ref_drawing_count INT GENERATED ALWAYS AS ( JSON_UNQUOTE( - JSON_EXTRACT(details, '$.drawingCount') + JSON_EXTRACT(details, '$.drawingCount') ) -) VIRTUAL; + ) VIRTUAL; ALTER TABLE rfa_revisions -ADD COLUMN schema_version INT DEFAULT 1 COMMENT 'Version ของ JSON Schema' AFTER details; +ADD COLUMN schema_version INT DEFAULT 1 COMMENT 'Version ของ JSON Schema' +AFTER details; CREATE INDEX idx_rfa_rev_v_drawing_count ON rfa_revisions (v_ref_drawing_count); + -- ... (ต่อท้ายไฟล์เดิม) -- ============================================================ -- ส่วนที่ 11: Unified Workflow Engine (Phase 6A/Phase 3) @@ -1208,65 +1282,66 @@ DROP TABLE IF EXISTS workflow_histories; DROP TABLE IF EXISTS workflow_instances; DROP TABLE IF EXISTS workflow_definitions; + -- 1. ตารางเก็บนิยาม Workflow (Definition / DSL) CREATE TABLE workflow_definitions ( - id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID ของ Workflow Definition', - workflow_code VARCHAR(50) NOT NULL COMMENT 'รหัส Workflow เช่น RFA_FLOW_V1, CORRESPONDENCE_FLOW_V1', - version INT NOT NULL DEFAULT 1 COMMENT 'หมายเลข Version', - description TEXT NULL COMMENT 'คำอธิบาย Workflow', - dsl JSON NOT NULL COMMENT 'นิยาม Workflow ต้นฉบับ (YAML/JSON Format)', - compiled JSON NOT NULL COMMENT 'โครงสร้าง Execution Tree ที่ Compile แล้ว', - is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', - -- ป้องกันการมี Workflow Code และ Version ซ้ำกัน - UNIQUE KEY uq_workflow_version (workflow_code, version) + id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID ของ Workflow Definition', + workflow_code VARCHAR(50) NOT NULL COMMENT 'รหัส Workflow เช่น RFA_FLOW_V1, CORRESPONDENCE_FLOW_V1', + version INT NOT NULL DEFAULT 1 COMMENT 'หมายเลข Version', + description TEXT NULL COMMENT 'คำอธิบาย Workflow', + dsl JSON NOT NULL COMMENT 'นิยาม Workflow ต้นฉบับ (YAML/JSON Format)', + compiled JSON NOT NULL COMMENT 'โครงสร้าง Execution Tree ที่ Compile แล้ว', + is_active BOOLEAN DEFAULT TRUE COMMENT 'สถานะการใช้งาน', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด', + -- ป้องกันการมี Workflow Code และ Version ซ้ำกัน + UNIQUE KEY uq_workflow_version (workflow_code, version) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางเก็บนิยามกฎการเดินเอกสาร (Workflow DSL)'; + -- สร้าง Index สำหรับการค้นหา Workflow ที่ Active ล่าสุดได้เร็วขึ้น -CREATE INDEX idx_workflow_active ON workflow_definitions ( - workflow_code, - is_active, - version -); +CREATE INDEX idx_workflow_active ON workflow_definitions (workflow_code, is_active, version); + -- 2. ตารางเก็บ Workflow Instance (สถานะเอกสารจริง) CREATE TABLE workflow_instances ( - id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID ของ Instance', - definition_id CHAR(36) NOT NULL COMMENT 'อ้างอิง Definition ที่ใช้', - entity_type VARCHAR(50) NOT NULL COMMENT 'ประเภทเอกสาร (rfa_revision, correspondence_revision, circulation)', - entity_id VARCHAR(50) NOT NULL COMMENT 'ID ของเอกสาร (String/Int)', - current_state VARCHAR(50) NOT NULL COMMENT 'สถานะปัจจุบัน', - STATUS ENUM( - 'ACTIVE', - 'COMPLETED', - 'CANCELLED', - 'TERMINATED' - ) DEFAULT 'ACTIVE' COMMENT 'สถานะภาพรวม', - context JSON NULL COMMENT 'ตัวแปร Context สำหรับตัดสินใจ', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT fk_wf_inst_def FOREIGN KEY (definition_id) REFERENCES workflow_definitions (id) ON DELETE CASCADE + id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID ของ Instance', + definition_id CHAR(36) NOT NULL COMMENT 'อ้างอิง Definition ที่ใช้', + entity_type VARCHAR(50) NOT NULL COMMENT 'ประเภทเอกสาร (rfa_revision, correspondence_revision, circulation)', + entity_id VARCHAR(50) NOT NULL COMMENT 'ID ของเอกสาร (String/Int)', + current_state VARCHAR(50) NOT NULL COMMENT 'สถานะปัจจุบัน', + STATUS ENUM( + 'ACTIVE', + 'COMPLETED', + 'CANCELLED', + 'TERMINATED' + ) DEFAULT 'ACTIVE' COMMENT 'สถานะภาพรวม', + context JSON NULL COMMENT 'ตัวแปร Context สำหรับตัดสินใจ', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_wf_inst_def FOREIGN KEY (definition_id) REFERENCES workflow_definitions (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางเก็บสถานะการเดินเรื่องของเอกสาร'; CREATE INDEX idx_wf_inst_entity ON workflow_instances (entity_type, entity_id); CREATE INDEX idx_wf_inst_state ON workflow_instances (current_state); + -- 3. ตารางเก็บประวัติ (Audit Log / History) CREATE TABLE workflow_histories ( - id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID', - instance_id CHAR(36) NOT NULL COMMENT 'อ้างอิง Instance', - from_state VARCHAR(50) NOT NULL COMMENT 'สถานะต้นทาง', - to_state VARCHAR(50) NOT NULL COMMENT 'สถานะปลายทาง', - ACTION VARCHAR(50) NOT NULL COMMENT 'Action ที่กระทำ', - action_by_user_id INT NULL COMMENT 'User ID ผู้กระทำ', - COMMENT TEXT NULL COMMENT 'ความเห็น', - metadata JSON NULL COMMENT 'Snapshot ข้อมูล ณ ขณะนั้น', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT fk_wf_hist_inst FOREIGN KEY (instance_id) REFERENCES workflow_instances (id) ON DELETE CASCADE + id CHAR(36) NOT NULL PRIMARY KEY COMMENT 'UUID', + instance_id CHAR(36) NOT NULL COMMENT 'อ้างอิง Instance', + from_state VARCHAR(50) NOT NULL COMMENT 'สถานะต้นทาง', + to_state VARCHAR(50) NOT NULL COMMENT 'สถานะปลายทาง', + ACTION VARCHAR(50) NOT NULL COMMENT 'Action ที่กระทำ', + action_by_user_id INT NULL COMMENT 'User ID ผู้กระทำ', + COMMENT TEXT NULL COMMENT 'ความเห็น', + metadata JSON NULL COMMENT 'Snapshot ข้อมูล ณ ขณะนั้น', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_wf_hist_inst FOREIGN KEY (instance_id) REFERENCES workflow_instances (id) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'ตารางประวัติการเปลี่ยนสถานะ Workflow'; CREATE INDEX idx_wf_hist_instance ON workflow_histories (instance_id); CREATE INDEX idx_wf_hist_user ON workflow_histories (action_by_user_id); + -- ============================================================ -- 5. PARTITIONING PREPARATION (Advance - Optional) -- ============================================================ @@ -1289,10 +1364,8 @@ CREATE INDEX idx_document_number_formats_project ON document_number_formats (pro CREATE INDEX idx_document_number_formats_type ON document_number_formats (correspondence_type_id); -CREATE INDEX idx_document_number_formats_project_type ON document_number_formats ( - project_id, - correspondence_type_id -); +CREATE INDEX idx_document_number_formats_project_type ON document_number_formats (project_id, correspondence_type_id); + -- Indexes for document_number_counters CREATE INDEX idx_document_number_counters_project ON document_number_counters (project_id); @@ -1301,14 +1374,17 @@ CREATE INDEX idx_document_number_counters_org ON document_number_counters (origi CREATE INDEX idx_document_number_counters_type ON document_number_counters (correspondence_type_id); CREATE INDEX idx_document_number_counters_year ON document_number_counters (current_year); + -- Indexes for tags CREATE INDEX idx_tags_name ON tags (tag_name); CREATE INDEX idx_tags_created_at ON tags (created_at); + -- Indexes for correspondence_tags CREATE INDEX idx_correspondence_tags_correspondence ON correspondence_tags (correspondence_id); CREATE INDEX idx_correspondence_tags_tag ON correspondence_tags (tag_id); + -- Indexes for audit_logs CREATE INDEX idx_audit_logs_user ON audit_logs (user_id); @@ -1319,6 +1395,7 @@ CREATE INDEX idx_audit_logs_entity ON audit_logs (entity_type, entity_id); CREATE INDEX idx_audit_logs_created_at ON audit_logs (created_at); CREATE INDEX idx_audit_logs_ip ON audit_logs (ip_address); + -- Indexes for notifications CREATE INDEX idx_notifications_user ON notifications (user_id); @@ -1329,10 +1406,12 @@ CREATE INDEX idx_notifications_read ON notifications (is_read); CREATE INDEX idx_notifications_entity ON notifications (entity_type, entity_id); CREATE INDEX idx_notifications_created_at ON notifications (created_at); + -- Indexes for search_indices CREATE INDEX idx_search_indices_entity ON search_indices (entity_type, entity_id); CREATE INDEX idx_search_indices_indexed_at ON search_indices (indexed_at); + -- Indexes for backup_logs CREATE INDEX idx_backup_logs_type ON backup_logs (backup_type); @@ -1341,36 +1420,34 @@ CREATE INDEX idx_backup_logs_status ON backup_logs (STATUS); CREATE INDEX idx_backup_logs_started_at ON backup_logs (started_at); CREATE INDEX idx_backup_logs_completed_at ON backup_logs (completed_at); + -- ===================================================== -- Additional Composite Indexes for Performance -- ===================================================== -- Composite index for document_number_counters for faster lookups CREATE INDEX idx_doc_counter_composite ON document_number_counters ( - project_id, - originator_organization_id, - correspondence_type_id, - current_year + project_id, + originator_organization_id, + correspondence_type_id, + current_year ); + -- Composite index for notifications for user-specific queries CREATE INDEX idx_notifications_user_unread ON notifications (user_id, is_read, created_at); + -- Composite index for audit_logs for reporting -CREATE INDEX idx_audit_logs_reporting ON audit_logs ( - created_at, - entity_type, - ACTION -); +CREATE INDEX idx_audit_logs_reporting ON audit_logs (created_at, entity_type, ACTION); + -- Composite index for search_indices for entity-based queries -CREATE INDEX idx_search_entities ON search_indices ( - entity_type, - entity_id, - indexed_at -); +CREATE INDEX idx_search_entities ON search_indices (entity_type, entity_id, indexed_at); + -- สร้าง Index สำหรับ Cleanup Job CREATE INDEX idx_attachments_temp_cleanup ON attachments (is_temporary, expires_at); CREATE INDEX idx_attachments_temp_id ON attachments (temp_id); CREATE INDEX idx_audit_request_id ON audit_logs (request_id); + -- ===================================================== -- SQL Script for LCBP3-DMS (V1.4.0) - MariaDB -- Generated from Data Dictionary @@ -1380,115 +1457,108 @@ CREATE INDEX idx_audit_request_id ON audit_logs (request_id); -- ===================================================== -- View แสดง Revision "ปัจจุบัน" ของ correspondences ทั้งหมด (ที่ไม่ใช่ RFA) CREATE VIEW v_current_correspondences AS -SELECT - c.id AS correspondence_id, - c.correspondence_number, - c.correspondence_type_id, - ct.type_code AS correspondence_type_code, - ct.type_name AS correspondence_type_name, - c.project_id, - p.project_code, - p.project_name, - c.originator_id, - org.organization_code AS originator_code, - org.organization_name AS originator_name, - cr.id AS revision_id, - cr.revision_number, - cr.revision_label, - cr.title, - cr.document_date, - cr.issued_date, - cr.received_date, - cr.due_date, - cr.correspondence_status_id, - cs.status_code, - cs.status_name, - cr.created_by, - u.username AS created_by_username, - cr.created_at AS revision_created_at -FROM - correspondences c - INNER JOIN correspondence_types ct ON c.correspondence_type_id = ct.id - INNER JOIN projects p ON c.project_id = p.id - LEFT JOIN organizations org ON c.originator_id = org.id - INNER JOIN correspondence_revisions cr ON c.id = cr.correspondence_id - INNER JOIN correspondence_status cs ON cr.correspondence_status_id = cs.id - LEFT JOIN users u ON cr.created_by = u.user_id -WHERE - cr.is_current = TRUE - AND c.correspondence_type_id NOT IN( - SELECT id - FROM correspondence_types - WHERE - type_code = 'RFA' - ) - AND c.deleted_at IS NULL; +SELECT c.id AS correspondence_id, + c.correspondence_number, + c.correspondence_type_id, + ct.type_code AS correspondence_type_code, + ct.type_name AS correspondence_type_name, + c.project_id, + p.project_code, + p.project_name, + c.originator_id, + org.organization_code AS originator_code, + org.organization_name AS originator_name, + cr.id AS revision_id, + cr.revision_number, + cr.revision_label, + cr.title, + cr.document_date, + cr.issued_date, + cr.received_date, + cr.due_date, + cr.correspondence_status_id, + cs.status_code, + cs.status_name, + cr.created_by, + u.username AS created_by_username, + cr.created_at AS revision_created_at +FROM correspondences c + INNER JOIN correspondence_types ct ON c.correspondence_type_id = ct.id + INNER JOIN projects p ON c.project_id = p.id + LEFT JOIN organizations org ON c.originator_id = org.id + INNER JOIN correspondence_revisions cr ON c.id = cr.correspondence_id + INNER JOIN correspondence_status cs ON cr.correspondence_status_id = cs.id + LEFT JOIN users u ON cr.created_by = u.user_id +WHERE cr.is_current = TRUE + AND c.correspondence_type_id NOT IN( + SELECT id + FROM correspondence_types + WHERE type_code = 'RFA' + ) + AND c.deleted_at IS NULL; + -- View แสดง Revision "ปัจจุบัน" ของ rfa_revisions ทั้งหมด CREATE VIEW v_current_rfas AS -SELECT - r.id AS rfa_id, - r.rfa_type_id, - rt.type_code AS rfa_type_code, - rt.type_name_th AS rfa_type_name_th, - rt.type_name_en AS rfa_type_name_en, - rr.correspondence_id, - c.correspondence_number, - c.project_id, - p.project_code, - p.project_name, - c.originator_id, - org.organization_name AS originator_name, - rr.id AS revision_id, - rr.revision_number, - rr.revision_label, - rr.title, - rr.document_date, - rr.issued_date, - rr.received_date, - rr.approved_date, - rr.rfa_status_code_id, - rsc.status_code AS rfa_status_code, - rsc.status_name AS rfa_status_name, - rr.rfa_approve_code_id, - rac.approve_code AS rfa_approve_code, - rac.approve_name AS rfa_approve_name, - rr.created_by, - u.username AS created_by_username, - rr.created_at AS revision_created_at -FROM - rfas r - INNER JOIN rfa_types rt ON r.rfa_type_id = rt.id - INNER JOIN rfa_revisions rr ON r.id = rr.rfa_id - INNER JOIN correspondences c ON rr.correspondence_id = c.id - INNER JOIN projects p ON c.project_id = p.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 - 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 -WHERE - rr.is_current = TRUE - AND r.deleted_at IS NULL - AND c.deleted_at IS NULL; +SELECT r.id AS rfa_id, + r.rfa_type_id, + rt.type_code AS rfa_type_code, + rt.type_name_th AS rfa_type_name_th, + rt.type_name_en AS rfa_type_name_en, + rr.correspondence_id, + c.correspondence_number, + c.project_id, + p.project_code, + p.project_name, + c.originator_id, + org.organization_name AS originator_name, + rr.id AS revision_id, + rr.revision_number, + rr.revision_label, + rr.title, + rr.document_date, + rr.issued_date, + rr.received_date, + rr.approved_date, + rr.rfa_status_code_id, + rsc.status_code AS rfa_status_code, + rsc.status_name AS rfa_status_name, + rr.rfa_approve_code_id, + rac.approve_code AS rfa_approve_code, + rac.approve_name AS rfa_approve_name, + rr.created_by, + u.username AS created_by_username, + rr.created_at AS revision_created_at +FROM rfas r + INNER JOIN rfa_types rt ON r.rfa_type_id = rt.id + INNER JOIN rfa_revisions rr ON r.id = rr.rfa_id + INNER JOIN correspondences c ON rr.correspondence_id = c.id + INNER JOIN projects p ON c.project_id = p.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 + 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 +WHERE rr.is_current = TRUE + AND r.deleted_at IS NULL + AND c.deleted_at IS NULL; + -- View แสดงความสัมพันธ์ทั้งหมดระหว่าง Contract, Project, และ Organization CREATE VIEW v_contract_parties_all AS -SELECT - c.id AS contract_id, - c.contract_code, - c.contract_name, - p.id AS project_id, - p.project_code, - p.project_name, - o.id AS organization_id, - o.organization_code, - o.organization_name, - co.role_in_contract -FROM - contracts c - INNER JOIN projects p ON c.project_id = p.id - INNER JOIN contract_organizations co ON c.id = co.contract_id - INNER JOIN organizations o ON co.organization_id = o.id -WHERE - c.is_active = TRUE; +SELECT c.id AS contract_id, + c.contract_code, + c.contract_name, + p.id AS project_id, + p.project_code, + p.project_name, + o.id AS organization_id, + o.organization_code, + o.organization_name, + co.role_in_contract +FROM contracts c + INNER JOIN projects p ON c.project_id = p.id + INNER JOIN contract_organizations co ON c.id = co.contract_id + INNER JOIN organizations o ON co.organization_id = o.id +WHERE c.is_active = TRUE; + -- ============================================================ -- View: v_user_tasks (Unified Workflow Engine Edition) -- ============================================================ @@ -1497,309 +1567,278 @@ WHERE -- ============================================================ CREATE OR REPLACE VIEW v_user_tasks AS SELECT -- 1. Workflow Instance Info - wi.id AS instance_id, - wd.workflow_code, - wi.current_state, - wi.status AS workflow_status, - wi.created_at AS assigned_at, - -- 2. Entity Info (Polymorphic Identity) - wi.entity_type, - wi.entity_id, - -- 3. Normalized Document Info (ดึงข้อมูลจริงจากตารางลูกตามประเภท) - -- ใช้ CASE WHEN เพื่อรวมคอลัมน์ที่ชื่อต่างกันให้เป็นชื่อกลาง (document_number, subject) - CASE - WHEN wi.entity_type = 'rfa_revision' THEN rfa_corr.correspondence_number - WHEN wi.entity_type = 'circulation' THEN circ.circulation_no - WHEN wi.entity_type = 'correspondence_revision' THEN corr_corr.correspondence_number - ELSE 'N/A' - END AS document_number, - CASE - WHEN wi.entity_type = 'rfa_revision' THEN rfa_rev.title - WHEN wi.entity_type = 'circulation' THEN circ.circulation_subject - WHEN wi.entity_type = 'correspondence_revision' THEN corr_rev.title - ELSE 'Unknown Document' - END AS subject, - -- 4. Context Info (สำหรับ Filter สิทธิ์การมองเห็นที่ Backend) - -- ดึงเป็น JSON String เพื่อให้ Backend ไป Parse หรือใช้ JSON_CONTAINS - JSON_UNQUOTE( - JSON_EXTRACT(wi.context, '$.ownerId') - ) AS owner_id, - JSON_EXTRACT(wi.context, '$.assigneeIds') AS assignee_ids_json -FROM - workflow_instances wi - 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' - AND wi.entity_id = CAST(rfa_rev.id AS CHAR) - LEFT JOIN correspondences rfa_corr ON rfa_rev.correspondence_id = rfa_corr.id -- 6. Joins for Circulation - LEFT JOIN circulations circ ON wi.entity_type = 'circulation' - 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' - AND wi.entity_id = CAST(corr_rev.id AS CHAR) - LEFT JOIN correspondences corr_corr ON corr_rev.correspondence_id = corr_corr.id -- 8. Filter เฉพาะงานที่ยัง Active อยู่ -WHERE - wi.status = 'ACTIVE'; + wi.id AS instance_id, + wd.workflow_code, + wi.current_state, + wi.status AS workflow_status, + wi.created_at AS assigned_at, + -- 2. Entity Info (Polymorphic Identity) + wi.entity_type, + wi.entity_id, + -- 3. Normalized Document Info (ดึงข้อมูลจริงจากตารางลูกตามประเภท) + -- ใช้ CASE WHEN เพื่อรวมคอลัมน์ที่ชื่อต่างกันให้เป็นชื่อกลาง (document_number, subject) + CASE + WHEN wi.entity_type = 'rfa_revision' THEN rfa_corr.correspondence_number + WHEN wi.entity_type = 'circulation' THEN circ.circulation_no + WHEN wi.entity_type = 'correspondence_revision' THEN corr_corr.correspondence_number + ELSE 'N/A' + END AS document_number, + CASE + WHEN wi.entity_type = 'rfa_revision' THEN rfa_rev.title + WHEN wi.entity_type = 'circulation' THEN circ.circulation_subject + WHEN wi.entity_type = 'correspondence_revision' THEN corr_rev.title + ELSE 'Unknown Document' + END AS subject, + -- 4. Context Info (สำหรับ Filter สิทธิ์การมองเห็นที่ Backend) + -- ดึงเป็น JSON String เพื่อให้ Backend ไป Parse หรือใช้ JSON_CONTAINS + JSON_UNQUOTE(JSON_EXTRACT(wi.context, '$.ownerId')) AS owner_id, + JSON_EXTRACT(wi.context, '$.assigneeIds') AS assignee_ids_json +FROM workflow_instances wi + 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' + AND wi.entity_id = CAST(rfa_rev.id AS CHAR) + LEFT JOIN correspondences rfa_corr ON rfa_rev.correspondence_id = rfa_corr.id -- 6. Joins for Circulation + LEFT JOIN circulations circ ON wi.entity_type = 'circulation' + 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' + AND wi.entity_id = CAST(corr_rev.id AS CHAR) + LEFT JOIN correspondences corr_corr ON corr_rev.correspondence_id = corr_corr.id -- 8. Filter เฉพาะงานที่ยัง Active อยู่ +WHERE wi.status = 'ACTIVE'; + -- View แสดง audit_logs พร้อมข้อมูล username และ email ของผู้กระทำ CREATE VIEW v_audit_log_details AS -SELECT al.audit_id, al.user_id, u.username, u.email, u.first_name, u.last_name, al.action, al.entity_type, al.entity_id, al.details_json, al.ip_address, al.user_agent, al.created_at +SELECT al.audit_id, + al.user_id, + u.username, + u.email, + u.first_name, + u.last_name, + al.action, + al.entity_type, + al.entity_id, + al.details_json, + al.ip_address, + al.user_agent, + al.created_at FROM audit_logs al - LEFT JOIN users u ON al.user_id = u.user_id; + LEFT JOIN users u ON al.user_id = u.user_id; + -- View รวมสิทธิ์ทั้งหมด (Global + Project) ของผู้ใช้ทุกคน CREATE VIEW v_user_all_permissions AS -- Global Permissions -SELECT - ua.user_id, - ua.role_id, - r.role_name, - rp.permission_id, - p.permission_name, - p.module, - p.scope_level, - ua.organization_id, - NULL AS project_id, - NULL AS contract_id, - 'GLOBAL' AS permission_scope -FROM - user_assignments ua - INNER JOIN roles r ON ua.role_id = r.role_id - INNER JOIN role_permissions rp ON ua.role_id = rp.role_id - INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Global scope -WHERE - p.is_active = 1 - AND ua.organization_id IS NULL - AND ua.project_id IS NULL - AND ua.contract_id IS NULL +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + NULL AS project_id, + NULL AS contract_id, + 'GLOBAL' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Global scope +WHERE p.is_active = 1 + AND ua.organization_id IS NULL + AND ua.project_id IS NULL + AND ua.contract_id IS NULL UNION ALL -- Organization-specific Permissions -SELECT - ua.user_id, - ua.role_id, - r.role_name, - rp.permission_id, - p.permission_name, - p.module, - p.scope_level, - ua.organization_id, - NULL AS project_id, - NULL AS contract_id, - 'ORGANIZATION' AS permission_scope -FROM - user_assignments ua - INNER JOIN roles r ON ua.role_id = r.role_id - INNER JOIN role_permissions rp ON ua.role_id = rp.role_id - INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Organization scope -WHERE - p.is_active = 1 - AND ua.organization_id IS NOT NULL - AND ua.project_id IS NULL - AND ua.contract_id IS NULL +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + NULL AS project_id, + NULL AS contract_id, + 'ORGANIZATION' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Organization scope +WHERE p.is_active = 1 + AND ua.organization_id IS NOT NULL + AND ua.project_id IS NULL + AND ua.contract_id IS NULL UNION ALL -- Project-specific Permissions -SELECT - ua.user_id, - ua.role_id, - r.role_name, - rp.permission_id, - p.permission_name, - p.module, - p.scope_level, - ua.organization_id, - ua.project_id, - NULL AS contract_id, - 'PROJECT' AS permission_scope -FROM - user_assignments ua - INNER JOIN roles r ON ua.role_id = r.role_id - INNER JOIN role_permissions rp ON ua.role_id = rp.role_id - INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Project scope -WHERE - p.is_active = 1 - AND ua.project_id IS NOT NULL - AND ua.contract_id IS NULL +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + ua.project_id, + NULL AS contract_id, + 'PROJECT' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Project scope +WHERE p.is_active = 1 + AND ua.project_id IS NOT NULL + AND ua.contract_id IS NULL UNION ALL -- Contract-specific Permissions -SELECT - ua.user_id, - ua.role_id, - r.role_name, - rp.permission_id, - p.permission_name, - p.module, - p.scope_level, - ua.organization_id, - ua.project_id, - ua.contract_id, - 'CONTRACT' AS permission_scope -FROM - user_assignments ua - INNER JOIN roles r ON ua.role_id = r.role_id - INNER JOIN role_permissions rp ON ua.role_id = rp.role_id - INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Contract scope -WHERE - p.is_active = 1 - AND ua.contract_id IS NOT NULL; +SELECT ua.user_id, + ua.role_id, + r.role_name, + rp.permission_id, + p.permission_name, + p.module, + p.scope_level, + ua.organization_id, + ua.project_id, + ua.contract_id, + 'CONTRACT' AS permission_scope +FROM user_assignments ua + INNER JOIN roles r ON ua.role_id = r.role_id + INNER JOIN role_permissions rp ON ua.role_id = rp.role_id + INNER JOIN permissions p ON rp.permission_id = p.permission_id -- Contract scope +WHERE p.is_active = 1 + AND ua.contract_id IS NOT NULL; + -- ===================================================== -- Additional Useful Views -- ===================================================== -- View แสดงเอกสารทั้งหมดที่มีไฟล์แนบ CREATE VIEW v_documents_with_attachments AS -SELECT - 'CORRESPONDENCE' AS document_type, - c.id AS document_id, - c.correspondence_number AS document_number, - c.project_id, - p.project_code, - p.project_name, - COUNT(ca.attachment_id) AS attachment_count, - MAX(a.created_at) AS latest_attachment_date -FROM - correspondences c - INNER JOIN projects p ON c.project_id = p.id - LEFT JOIN correspondence_attachments ca ON c.id = ca.correspondence_id - LEFT JOIN attachments a ON ca.attachment_id = a.id -WHERE - c.deleted_at IS NULL -GROUP BY - c.id, - c.correspondence_number, - c.project_id, - p.project_code, - p.project_name +SELECT 'CORRESPONDENCE' AS document_type, + c.id AS document_id, + c.correspondence_number AS document_number, + c.project_id, + p.project_code, + p.project_name, + COUNT(ca.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM correspondences c + INNER JOIN projects p ON c.project_id = p.id + LEFT JOIN correspondence_attachments ca ON c.id = ca.correspondence_id + LEFT JOIN attachments a ON ca.attachment_id = a.id +WHERE c.deleted_at IS NULL +GROUP BY c.id, + c.correspondence_number, + c.project_id, + p.project_code, + p.project_name UNION ALL -SELECT - 'CIRCULATION' AS document_type, - circ.id AS document_id, - circ.circulation_no AS document_number, - corr.project_id, - p.project_code, - p.project_name, - COUNT(ca.attachment_id) AS attachment_count, - MAX(a.created_at) AS latest_attachment_date -FROM - circulations circ - INNER JOIN correspondences corr ON circ.correspondence_id = corr.id - INNER JOIN projects p ON corr.project_id = p.id - LEFT JOIN circulation_attachments ca ON circ.id = ca.circulation_id - LEFT JOIN attachments a ON ca.attachment_id = a.id -GROUP BY - circ.id, - circ.circulation_no, - corr.project_id, - p.project_code, - p.project_name +SELECT 'CIRCULATION' AS document_type, + circ.id AS document_id, + circ.circulation_no AS document_number, + corr.project_id, + p.project_code, + p.project_name, + COUNT(ca.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM circulations circ + INNER JOIN correspondences corr ON circ.correspondence_id = corr.id + INNER JOIN projects p ON corr.project_id = p.id + LEFT JOIN circulation_attachments ca ON circ.id = ca.circulation_id + LEFT JOIN attachments a ON ca.attachment_id = a.id +GROUP BY circ.id, + circ.circulation_no, + corr.project_id, + p.project_code, + p.project_name UNION ALL -SELECT - 'SHOP_DRAWING' AS document_type, - sdr.id AS document_id, - sd.drawing_number AS document_number, - sd.project_id, - p.project_code, - p.project_name, - COUNT(sdra.attachment_id) AS attachment_count, - MAX(a.created_at) AS latest_attachment_date -FROM - shop_drawing_revisions sdr - INNER JOIN shop_drawings sd ON sdr.shop_drawing_id = sd.id - INNER JOIN projects p ON sd.project_id = p.id - LEFT JOIN shop_drawing_revision_attachments sdra ON sdr.id = sdra.shop_drawing_revision_id - LEFT JOIN attachments a ON sdra.attachment_id = a.id -WHERE - sd.deleted_at IS NULL -GROUP BY - sdr.id, - sd.drawing_number, - sd.project_id, - p.project_code, - p.project_name +SELECT 'SHOP_DRAWING' AS document_type, + sdr.id AS document_id, + sd.drawing_number AS document_number, + sd.project_id, + p.project_code, + p.project_name, + COUNT(sdra.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM shop_drawing_revisions sdr + INNER JOIN shop_drawings sd ON sdr.shop_drawing_id = sd.id + INNER JOIN projects p ON sd.project_id = p.id + LEFT JOIN shop_drawing_revision_attachments sdra ON sdr.id = sdra.shop_drawing_revision_id + LEFT JOIN attachments a ON sdra.attachment_id = a.id +WHERE sd.deleted_at IS NULL +GROUP BY sdr.id, + sd.drawing_number, + sd.project_id, + p.project_code, + p.project_name UNION ALL -SELECT - 'CONTRACT_DRAWING' AS document_type, - cd.id AS document_id, - cd.condwg_no AS document_number, - cd.project_id, - p.project_code, - p.project_name, - COUNT(cda.attachment_id) AS attachment_count, - MAX(a.created_at) AS latest_attachment_date -FROM - contract_drawings cd - INNER JOIN projects p ON cd.project_id = p.id - LEFT JOIN contract_drawing_attachments cda ON cd.id = cda.contract_drawing_id - LEFT JOIN attachments a ON cda.attachment_id = a.id -WHERE - cd.deleted_at IS NULL -GROUP BY - cd.id, - cd.condwg_no, - cd.project_id, - p.project_code, - p.project_name; +SELECT 'CONTRACT_DRAWING' AS document_type, + cd.id AS document_id, + cd.condwg_no AS document_number, + cd.project_id, + p.project_code, + p.project_name, + COUNT(cda.attachment_id) AS attachment_count, + MAX(a.created_at) AS latest_attachment_date +FROM contract_drawings cd + INNER JOIN projects p ON cd.project_id = p.id + LEFT JOIN contract_drawing_attachments cda ON cd.id = cda.contract_drawing_id + LEFT JOIN attachments a ON cda.attachment_id = a.id +WHERE cd.deleted_at IS NULL +GROUP BY cd.id, + cd.condwg_no, + cd.project_id, + p.project_code, + p.project_name; + -- View แสดงสถิติเอกสารตามประเภทและสถานะ CREATE VIEW v_document_statistics AS -SELECT - p.id AS project_id, - p.project_code, - p.project_name, - ct.id AS correspondence_type_id, - ct.type_code, - ct.type_name, - cs.id AS status_id, - cs.status_code, - cs.status_name, - COUNT(DISTINCT c.id) AS document_count, - COUNT(DISTINCT cr.id) AS revision_count -FROM - projects p - CROSS JOIN correspondence_types ct - CROSS JOIN correspondence_status cs - LEFT JOIN correspondences c ON p.id = c.project_id - AND ct.id = c.correspondence_type_id - LEFT JOIN correspondence_revisions cr ON c.id = cr.correspondence_id - AND cs.id = cr.correspondence_status_id - AND cr.is_current = TRUE -WHERE - p.is_active = 1 - AND ct.is_active = 1 - AND cs.is_active = 1 -GROUP BY - p.id, - p.project_code, - p.project_name, - ct.id, - ct.type_code, - ct.type_name, - cs.id, - cs.status_code, - cs.status_name; +SELECT p.id AS project_id, + p.project_code, + p.project_name, + ct.id AS correspondence_type_id, + ct.type_code, + ct.type_name, + cs.id AS status_id, + cs.status_code, + cs.status_name, + COUNT(DISTINCT c.id) AS document_count, + COUNT(DISTINCT cr.id) AS revision_count +FROM projects p + CROSS JOIN correspondence_types ct + CROSS JOIN correspondence_status cs + LEFT JOIN correspondences c ON p.id = c.project_id + AND ct.id = c.correspondence_type_id + LEFT JOIN correspondence_revisions cr ON c.id = cr.correspondence_id + AND cs.id = cr.correspondence_status_id + AND cr.is_current = TRUE +WHERE p.is_active = 1 + AND ct.is_active = 1 + AND cs.is_active = 1 +GROUP BY p.id, + p.project_code, + p.project_name, + ct.id, + ct.type_code, + ct.type_name, + cs.id, + cs.status_code, + cs.status_name; + -- ===================================================== -- Indexes for View Performance Optimization -- ===================================================== -- Indexes for v_current_correspondences performance -CREATE INDEX idx_correspondences_type_project ON correspondences ( - correspondence_type_id, - project_id -); +CREATE INDEX idx_correspondences_type_project ON correspondences (correspondence_type_id, project_id); -CREATE INDEX idx_corr_revisions_current_status ON correspondence_revisions ( - is_current, - correspondence_status_id -); +CREATE INDEX idx_corr_revisions_current_status ON correspondence_revisions (is_current, correspondence_status_id); CREATE INDEX idx_corr_revisions_correspondence_current ON correspondence_revisions (correspondence_id, is_current); + -- 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_current_status ON rfa_revisions (is_current, rfa_status_code_id); CREATE INDEX idx_rfa_revisions_rfa_current ON rfa_revisions (rfa_id, is_current); + -- 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); -CREATE INDEX idx_corr_revisions_status_current ON correspondence_revisions ( - correspondence_status_id, - is_current -); +CREATE INDEX idx_corr_revisions_status_current ON correspondence_revisions (correspondence_status_id, is_current); -SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file +SET FOREIGN_KEY_CHECKS = 1; diff --git a/docs/8_lcbp3_v1_5_1_seed.sql b/docs/8_lcbp3_v1_5_1_seed.sql index 297a460..63c3147 100644 --- a/docs/8_lcbp3_v1_5_1_seed.sql +++ b/docs/8_lcbp3_v1_5_1_seed.sql @@ -1,534 +1,615 @@ -INSERT INTO organization_roles (id, role_name) VALUES - (1, 'OWNER'), +INSERT INTO organization_roles (id, role_name) +VALUES (1, 'OWNER'), (2, 'DESIGNER'), (3, 'CONSULTANT'), (4, 'CONTRACTOR'), (5, 'THIRD PARTY'), (6, 'GUEST'); -INSERT INTO organizations ( id, organization_code, organization_name, role_id ) VALUES - ( 1, 'กทท.', 'การท่าเรือแห่งประเทศไทย', 1 ), - ( 10, 'สคฉ.3', 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3', 1 ), - ( 11, 'สคฉ.3-01', 'ตรวจรับพัสดุ ที่ปรึกษาควบคุมงาน', 1 ), - ( 12, 'สคฉ.3-02', 'ตรวจรับพัสดุ งานทางทะเล', 1 ), - ( 13, 'สคฉ.3-03', 'ตรวจรับพัสดุ อาคารและระบบสาธารณูปโภค', 1 ), - ( 14, 'สคฉ.3-04', 'ตรวจรับพัสดุ ตรวจสอบผลกระทบสิ่งแวดล้อม', 1 ), - ( 15, 'สคฉ.3-05', 'ตรวจรับพัสดุ เยียวยาการประมง', 1 ), - ( 16, 'สคฉ.3-06', 'ตรวจรับพัสดุ งานก่อสร้าง ส่วนที่ 3', 1 ), - ( 17, 'สคฉ.3-07', 'ตรวจรับพัสดุ งานก่อสร้าง ส่วนที่ 4', 1 ), - ( 18, 'สคฉ.3-xx', 'ตรวจรับพัสดุ ที่ปรึกษาออกแบบ ส่วนที่ 4', 1 ), - ( 21, 'TEAM', 'Designer Consulting Ltd.', 2 ), - ( 22, 'คคง.', 'Construction Supervision Ltd.', 3 ), - ( 41, 'ผรม.1', 'Contractor งานทางทะเล', 4 ), - ( 42, 'ผรม.2', 'Contractor อาคารและระบบ', 4 ), - ( 43, 'ผรม.3', 'Contractor งานก่อสร้าง ส่วนที่ 3', 4 ), - ( 44, 'ผรม.4', 'Contractor งานก่อสร้าง ส่วนที่ 4', 4 ), - ( 31, 'EN', 'Third Party Environment', 5 ), - ( 32, 'CAR', 'Third Party Fishery Care', 5 ); + +INSERT INTO organizations ( + id, + organization_code, + organization_name, + role_id + ) +VALUES (1, 'กทท.', 'การท่าเรือแห่งประเทศไทย', 1), + ( + 10, + 'สคฉ.3', + 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3', + 1 + ), + ( + 11, + 'สคฉ.3-01', + 'ตรวจรับพัสดุ ที่ปรึกษาควบคุมงาน', + 1 + ), + (12, 'สคฉ.3-02', 'ตรวจรับพัสดุ งานทางทะเล', 1), + ( + 13, + 'สคฉ.3-03', + 'ตรวจรับพัสดุ อาคารและระบบสาธารณูปโภค', + 1 + ), + ( + 14, + 'สคฉ.3-04', + 'ตรวจรับพัสดุ ตรวจสอบผลกระทบสิ่งแวดล้อม', + 1 + ), + ( + 15, + 'สคฉ.3-05', + 'ตรวจรับพัสดุ เยียวยาการประมง', + 1 + ), + ( + 16, + 'สคฉ.3-06', + 'ตรวจรับพัสดุ งานก่อสร้าง ส่วนที่ 3', + 1 + ), + ( + 17, + 'สคฉ.3-07', + 'ตรวจรับพัสดุ งานก่อสร้าง ส่วนที่ 4', + 1 + ), + ( + 18, + 'สคฉ.3-xx', + 'ตรวจรับพัสดุ ที่ปรึกษาออกแบบ ส่วนที่ 4', + 1 + ), + ( + 21, + 'TEAM', + 'Designer Consulting Ltd.', + 2 + ), + ( + 22, + 'คคง.', + 'Construction Supervision Ltd.', + 3 + ), + ( + 41, + 'ผรม.1', + 'Contractor งานทางทะเล', + 4 + ), + ( + 42, + 'ผรม.2', + 'Contractor งานก่อสร้าง', + 4 + ), + ( + 43, + 'ผรม.3', + 'Contractor งานก่อสร้าง ส่วนที่ 3', + 4 + ), + ( + 44, + 'ผรม.4', + 'Contractor งานก่อสร้าง ส่วนที่ 4', + 4 + ), + ( + 31, + 'EN', + 'Third Party Environment', + 5 + ), + ( + 32, + 'CAR', + 'Third Party Fishery Care', + 5 + ); + -- Seed project INSERT INTO projects (project_code, project_name) VALUES ( - 'LCBP3', - 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)' - ), - ( - 'LCBP3-C1', - 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1) งานก่อสร้างงานทางทะเล' - ), - ( - 'LCBP3-C2', - 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และระบบสาธารณูปโภค' - ), - ( - 'LCBP3-C3', - 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 3) งานก่อสร้าง' - ), - ( - 'LCBP3-C4', - 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง' - ), - ( - 'LCBP3-EN', - 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง' - ); + 'LCBP3', + 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)' + ), + ( + 'LCBP3-C1', + 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1) งานก่อสร้างงานทางทะเล' + ), + ( + 'LCBP3-C2', + 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และระบบสาธารณูปโภค' + ), + ( + 'LCBP3-C3', + 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 3) งานก่อสร้าง' + ), + ( + 'LCBP3-C4', + 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง' + ), + ( + 'LCBP3-EN', + 'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง' + ); + -- Seed contract -- ใช้ Subquery เพื่อดึง project_id มาเชื่อมโยง ทำให้ไม่ต้องมานั่งจัดการ ID ด้วยตัวเอง INSERT INTO contracts ( - contract_code, - contract_name, - project_id, - is_active - ) + contract_code, + contract_name, + project_id, + is_active + ) VALUES ( - 'LCBP3-DS', - 'งานจ้างที่ปรีกษาออกแบบ โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)', - ( - SELECT id - FROM projects - WHERE project_code = 'LCBP3' - ), - TRUE - ), + 'LCBP3-DS', + 'งานจ้างที่ปรีกษาออกแบบ โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)', ( - 'LCBP3-PS', - 'งานจ้างที่ปรีกษาควบคุมงาน โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)', - ( - SELECT id - FROM projects - WHERE project_code = 'LCBP3' - ), - TRUE + SELECT id + FROM projects + WHERE project_code = 'LCBP3' ), + TRUE + ), + ( + 'LCBP3-PS', + 'งานจ้างที่ปรีกษาควบคุมงาน โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)', ( - 'LCBP3-C1', - 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1) งานก่อสร้างงานทางทะเล', - ( - SELECT id - FROM projects - WHERE project_code = 'LCBP3-C1' - ), - TRUE + SELECT id + FROM projects + WHERE project_code = 'LCBP3' ), + TRUE + ), + ( + 'LCBP3-C1', + 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1) งานก่อสร้างงานทางทะเล', ( - 'LCBP3-C2', - 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และระบบสาธารณูปโภค', - ( - SELECT id - FROM projects - WHERE project_code = 'LCBP3-C2' - ), - TRUE + SELECT id + FROM projects + WHERE project_code = 'LCBP3-C1' ), + TRUE + ), + ( + 'LCBP3-C2', + 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 2) งานก่อสร้างอาคาร ท่าเทียบเรือ ระบบถนน และระบบสาธารณูปโภค', ( - 'LCBP3-C3', - 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 3) งานก่อสร้าง', - ( - SELECT id - FROM projects - WHERE project_code = 'LCBP3-C3' - ), - TRUE + SELECT id + FROM projects + WHERE project_code = 'LCBP3-C2' ), + TRUE + ), + ( + 'LCBP3-C3', + 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 3) งานก่อสร้าง', ( - 'LCBP3-C4', - 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง', - ( - SELECT id - FROM projects - WHERE project_code = 'LCBP3-C4' - ), - TRUE + SELECT id + FROM projects + WHERE project_code = 'LCBP3-C3' ), + TRUE + ), + ( + 'LCBP3-C4', + 'งานก่อสร้าง โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง', ( - 'LCBP3-EN', - 'งานจ้างเหมาตรวจสอบผลกระทบสิ่งแวดล้อมนะหว่างงานก่อสร้างโครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)', - ( - SELECT id - FROM projects - WHERE project_code = 'LCBP3' - ), - TRUE - ); + SELECT id + FROM projects + WHERE project_code = 'LCBP3-C4' + ), + TRUE + ), + ( + 'LCBP3-EN', + 'งานจ้างเหมาตรวจสอบผลกระทบสิ่งแวดล้อมนะหว่างงานก่อสร้างโครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 1-4)', + ( + SELECT id + FROM projects + WHERE project_code = 'LCBP3' + ), + TRUE + ); + -- Seed user -- Initial SUPER_ADMIN user INSERT INTO users ( - `user_id`, - `username`, - `password_hash`, - `first_name`, - `last_name`, - `email`, - `line_id`, - `primary_organization_id` - ) + `user_id`, + `username`, + `password_hash`, + `first_name`, + `last_name`, + `email`, + `line_id`, + `primary_organization_id` + ) VALUES ( - 1, - 'superadmin', - '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW', - 'Super', - 'Admin', - 'superadmin @example.com', - NULL, - NULL - ), - ( - 2, - 'admin', - '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW', - 'Admin', - 'คคง.', - 'admin@example.com', - NULL, - 1 - ), - ( - 3, - 'editor01', - '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW', - 'DC', - 'C1', - 'editor01 @example.com', - NULL, - 41 - ), - ( - 4, - 'viewer01', - '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW', - 'Viewer', - 'สคฉ.03', - 'viewer01 @example.com', - NULL, - 10 - ); + 1, + 'superadmin', + '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW', + 'Super', + 'Admin', + 'superadmin @example.com', + NULL, + NULL + ), + ( + 2, + 'admin', + '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW', + 'Admin', + 'คคง.', + 'admin@example.com', + NULL, + 1 + ), + ( + 3, + 'editor01', + '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW', + 'DC', + 'C1', + 'editor01 @example.com', + NULL, + 41 + ), + ( + 4, + 'viewer01', + '$2b$10$E6d5k.f46jr.POGWKHhiQ.X1ZsFrMpZox//sCxeOiLUULGuAHO0NW', + 'Viewer', + 'สคฉ.03', + 'viewer01 @example.com', + NULL, + 10 + ); + -- ========================================================== -- Seed Roles (บทบาทพื้นฐาน 5 บทบาท ตาม Req 4.3) -- ========================================================== -- 1. Superadmin (Global) INSERT INTO roles (role_id, role_name, scope, description) VALUES ( - 1, - 'Superadmin', - 'Global', - 'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global' - ).-- 2. Org Admin (Organization) - ( - 2, - 'Org Admin', - 'Organization', - 'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร' - ), - -- 3. Document Control (Organization) - ( - 3, - 'Document Control', - 'Organization', - 'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร' - ), - -- 4. Editor (Organization) - ( - 4, - 'Editor', - 'Organization', - 'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย' - ), - -- 5. Viewer (Organization) - ( - 5, - 'Viewer', - 'Organization', - 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น' - ), - -- 6. Project Manager (Project) - ( - 6, - 'Project Manager', - 'Project', - 'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ' - ), - -- 7. Contract Admin (Contract) - ( - 7, - 'Contract Admin', - 'Contract', - 'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา' - ); + 1, + 'Superadmin', + 'Global', + 'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global' + ), + -- 2. Org Admin (Organization) + ( + 2, + 'Org Admin', + 'Organization', + 'ผู้ดูแลองค์กร: จัดการผู้ใช้ในองค์กร, จัดการบทบาท / สิทธิ์ภายในองค์กร, และดูรายงานขององค์กร' + ), + -- 3. Document Control (Organization) + ( + 3, + 'Document Control', + 'Organization', + 'ควบคุมเอกสารขององค์กร: เพิ่ม / แก้ไข / ลบเอกสาร, และกำหนดสิทธิ์เอกสารภายในองค์กร' + ), + -- 4. Editor (Organization) + ( + 4, + 'Editor', + 'Organization', + 'ผู้แก้ไขเอกสารขององค์กร: เพิ่ม / แก้ไขเอกสารที่ได้รับมอบหมาย' + ), + -- 5. Viewer (Organization) + ( + 5, + 'Viewer', + 'Organization', + 'ผู้ดูเอกสารขององค์กร: ดูเอกสารที่มีสิทธิ์เข้าถึงเท่านั้น' + ), + -- 6. Project Manager (Project) + ( + 6, + 'Project Manager', + 'Project', + 'ผู้จัดการโครงการ: จัดการสมาชิกในโครงการ, สร้าง / จัดการสัญญาในโครงการ, และดูรายงานโครงการ' + ), + -- 7. Contract Admin (Contract) + ( + 7, + 'Contract Admin', + 'Contract', + 'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา' + ); + -- ===================================================== -- 2. Seed Permissions (สิทธิ์การใช้งานทั้งหมด) -- สิทธิ์ระดับระบบและการจัดการหลัก (System & Master Data) -- ===================================================== INSERT INTO permissions ( - permission_id, - permission_name, - description - ) + permission_id, + permission_name, + description + ) VALUES ( - 1, - 'system.manage_all', - 'ทำทุกอย่างในระบบ (Superadmin Power)' - ), - -- การจัดการองค์กร - ( - 2, - 'organization.create', - 'สร้างองค์กรใหม่' - ), - ( - 3, - 'organization.edit', - 'แก้ไขข้อมูลองค์กร' - ), - ( - 4, - 'organization.delete', - 'ลบองค์กร' - ), - ( - 5, - 'organization.view', - 'ดูรายการองค์กร' - ), - -- การจัดการโครงการ - ( - 6, - 'project.create', - 'สร้างโครงการใหม่' - ), - ( - 7, - 'project.edit', - 'แก้ไขข้อมูลโครงการ' - ), - ( - 8, - 'project.delete', - 'ลบโครงการ' - ), - ( - 9, - 'project.view', - 'ดูรายการโครงการ' - ), - -- การจัดการบทบาทและสิทธิ์ (Roles & Permissions) - ( - 10, - 'role.create', - 'สร้างบทบาท (Role) ใหม่' - ), - ( - 11, - 'role.edit', - 'แก้ไขบทบาท (Role)' - ), - ( - 12, - 'role.delete', - 'ลบบทบาท (Role)' - ), - ( - 13, - 'permission.assign', - 'มอบสิทธิ์ให้กับบทบาท (Role)' - ), - -- การจัดการข้อมูลหลัก (Master Data) - ( - 14, - 'master_data.document_type.manage', - 'จัดการประเภทเอกสาร (Document Types)' - ), - ( - 15, - 'master_data.document_status.manage', - 'จัดการสถานะเอกสาร (Document Statuses)' - ), - ( - 16, - 'master_data.drawing_category.manage', - 'จัดการหมวดหมู่แบบ (Drawing Categories)' - ), - ( - 17, - 'master_data.tag.manage', - 'จัดการ Tags' - ), - -- การจัดการผู้ใช้งาน - ( - 18, - 'user.create', - 'สร้างผู้ใช้งานใหม่' - ), - ( - 19, - 'user.edit', - 'แก้ไขข้อมูลผู้ใช้งาน' - ), - ( - 20, - 'user.delete', - 'ลบ / ปิดการใช้งานผู้ใช้' - ), - ( - 21, - 'user.view', - 'ดูข้อมูลผู้ใช้งาน' - ), - ( - 22, - 'user.assign_organization', - 'มอบผู้ใช้งานให้กับองค์กร' - ); + 1, + 'system.manage_all', + 'ทำทุกอย่างในระบบ (Superadmin Power)' + ), + -- การจัดการองค์กร + ( + 2, + 'organization.create', + 'สร้างองค์กรใหม่' + ), + ( + 3, + 'organization.edit', + 'แก้ไขข้อมูลองค์กร' + ), + ( + 4, + 'organization.delete', + 'ลบองค์กร' + ), + ( + 5, + 'organization.view', + 'ดูรายการองค์กร' + ), + -- การจัดการโครงการ + ( + 6, + 'project.create', + 'สร้างโครงการใหม่' + ), + ( + 7, + 'project.edit', + 'แก้ไขข้อมูลโครงการ' + ), + (8, 'project.delete', 'ลบโครงการ'), + ( + 9, + 'project.view', + 'ดูรายการโครงการ' + ), + -- การจัดการบทบาทและสิทธิ์ (Roles & Permissions) + ( + 10, + 'role.create', + 'สร้างบทบาท (Role) ใหม่' + ), + ( + 11, + 'role.edit', + 'แก้ไขบทบาท (Role)' + ), + (12, 'role.delete', 'ลบบทบาท (Role)'), + ( + 13, + 'permission.assign', + 'มอบสิทธิ์ให้กับบทบาท (Role)' + ), + -- การจัดการข้อมูลหลัก (Master Data) + ( + 14, + 'master_data.document_type.manage', + 'จัดการประเภทเอกสาร (Document Types)' + ), + ( + 15, + 'master_data.document_status.manage', + 'จัดการสถานะเอกสาร (Document Statuses)' + ), + ( + 16, + 'master_data.drawing_category.manage', + 'จัดการหมวดหมู่แบบ (Drawing Categories)' + ), + ( + 17, + 'master_data.tag.manage', + 'จัดการ Tags' + ), + -- การจัดการผู้ใช้งาน + ( + 18, + 'user.create', + 'สร้างผู้ใช้งานใหม่' + ), + ( + 19, + 'user.edit', + 'แก้ไขข้อมูลผู้ใช้งาน' + ), + ( + 20, + 'user.delete', + 'ลบ / ปิดการใช้งานผู้ใช้' + ), + ( + 21, + 'user.view', + 'ดูข้อมูลผู้ใช้งาน' + ), + ( + 22, + 'user.assign_organization', + 'มอบผู้ใช้งานให้กับองค์กร' + ); + -- ===================================================== -- == 2. สิทธิ์การจัดการโครงการและสัญญา (Project & Contract) == -- ===================================================== INSERT INTO permissions ( - permission_id, - permission_name, - description - ) + permission_id, + permission_name, + description + ) VALUES ( - 23, - 'project.manage_members', - 'จัดการสมาชิกในโครงการ (เชิญ / ถอดสมาชิก)' - ), - ( - 24, - 'project.create_contracts', - 'สร้างสัญญาในโครงการ' - ), - ( - 25, - 'project.manage_contracts', - 'จัดการสัญญาในโครงการ' - ), - ( - 26, - 'project.view_reports', - 'ดูรายงานระดับโครงการ' - ), - ( - 27, - 'contract.manage_members', - 'จัดการสมาชิกในสัญญา' - ), - ( - 28, - 'contract.view', - 'ดูข้อมูลสัญญา' - ); + 23, + 'project.manage_members', + 'จัดการสมาชิกในโครงการ (เชิญ / ถอดสมาชิก)' + ), + ( + 24, + 'project.create_contracts', + 'สร้างสัญญาในโครงการ' + ), + ( + 25, + 'project.manage_contracts', + 'จัดการสัญญาในโครงการ' + ), + ( + 26, + 'project.view_reports', + 'ดูรายงานระดับโครงการ' + ), + ( + 27, + 'contract.manage_members', + 'จัดการสมาชิกในสัญญา' + ), + ( + 28, + 'contract.view', + 'ดูข้อมูลสัญญา' + ); + -- ===================================================== -- == 3. สิทธิ์การจัดการเอกสาร (Document Management) == -- ===================================================== -- สิทธิ์ทั่วไปสำหรับเอกสารทุกประเภท INSERT INTO permissions ( - permission_id, - permission_name, - description - ) + permission_id, + permission_name, + description + ) VALUES ( - 29, - 'document.create_draft', - 'สร้างเอกสารในสถานะฉบับร่าง (Draft) ' - ), - ( - 30, - 'document.submit', - 'ส่งเอกสาร (Submitted)' - ), - ( - 31, - 'document.view', - 'ดูเอกสาร' - ), - ( - 32, - 'document.edit', - 'แก้ไขเอกสาร (ทั่วไป)' - ), - ( - 33, - 'document.admin_edit', - 'แก้ไข / ถอน / ยกเลิกเอกสารที่ส่งแล้ว (Admin Power) ' - ), - ( - 34, - 'document.delete', - 'ลบเอกสาร' - ), - ( - 35, - 'document.attach', - 'จัดการไฟล์แนบ (อัปโหลด / ลบ) ' - ), - -- สิทธิ์เฉพาะสำหรับ Correspondence - ( - 36, - 'correspondence.create', - 'สร้างเอกสารโต้ตอบ (Correspondence) ' - ), - -- สิทธิ์เฉพาะสำหรับ Request for Approval (RFA) - ( - 37, - 'rfa.create', - 'สร้างเอกสารขออนุมัติ (RFA)' - ), - ( - 38, - 'rfa.manage_shop_drawings', - 'จัดการข้อมูล Shop Drawing และ Contract Drawing ที่เกี่ยวข้อง' - ), - -- สิทธิ์เฉพาะสำหรับ Shop Drawing & Contract Drawing - ( - 39, - 'drawing.create', - 'สร้าง / แก้ไขข้อมูลแบบ (Shop / Contract Drawing)' - ), - -- สิทธิ์เฉพาะสำหรับ Transmittal - ( - 40, - 'transmittal.create', - 'สร้างเอกสารนำส่ง (Transmittal)' - ), - -- สิทธิ์เฉพาะสำหรับ Circulation Sheet (ใบเวียน) - ( - 41, - 'circulation.create', - 'สร้างใบเวียนเอกสาร (Circulation)' - ), - ( - 42, - 'circulation.respond', - 'ตอบกลับใบเวียน (Main / Action)' - ), - ( - 43, - 'circulation.acknowledge', - 'รับทราบใบเวียน (Information)' - ), - ( - 44, - 'circulation.close', - 'ปิดใบเวียน' - ); + 29, + 'document.create_draft', + 'สร้างเอกสารในสถานะฉบับร่าง (Draft) ' + ), + ( + 30, + 'document.submit', + 'ส่งเอกสาร (Submitted)' + ), + (31, 'document.view', 'ดูเอกสาร'), + ( + 32, + 'document.edit', + 'แก้ไขเอกสาร (ทั่วไป)' + ), + ( + 33, + 'document.admin_edit', + 'แก้ไข / ถอน / ยกเลิกเอกสารที่ส่งแล้ว (Admin Power) ' + ), + (34, 'document.delete', 'ลบเอกสาร'), + ( + 35, + 'document.attach', + 'จัดการไฟล์แนบ (อัปโหลด / ลบ) ' + ), + -- สิทธิ์เฉพาะสำหรับ Correspondence + ( + 36, + 'correspondence.create', + 'สร้างเอกสารโต้ตอบ (Correspondence) ' + ), + -- สิทธิ์เฉพาะสำหรับ Request for Approval (RFA) + ( + 37, + 'rfa.create', + 'สร้างเอกสารขออนุมัติ (RFA)' + ), + ( + 38, + 'rfa.manage_shop_drawings', + 'จัดการข้อมูล Shop Drawing และ Contract Drawing ที่เกี่ยวข้อง' + ), + -- สิทธิ์เฉพาะสำหรับ Shop Drawing & Contract Drawing + ( + 39, + 'drawing.create', + 'สร้าง / แก้ไขข้อมูลแบบ (Shop / Contract Drawing)' + ), + -- สิทธิ์เฉพาะสำหรับ Transmittal + ( + 40, + 'transmittal.create', + 'สร้างเอกสารนำส่ง (Transmittal)' + ), + -- สิทธิ์เฉพาะสำหรับ Circulation Sheet (ใบเวียน) + ( + 41, + 'circulation.create', + 'สร้างใบเวียนเอกสาร (Circulation)' + ), + ( + 42, + 'circulation.respond', + 'ตอบกลับใบเวียน (Main / Action)' + ), + ( + 43, + 'circulation.acknowledge', + 'รับทราบใบเวียน (Information)' + ), + ( + 44, + 'circulation.close', + 'ปิดใบเวียน' + ); + -- ===================================================== -- == 4. สิทธิ์การจัดการ Workflow == -- ===================================================== INSERT INTO permissions ( - permission_id, - permission_name, - description - ) + permission_id, + permission_name, + description + ) VALUES ( - 45, - 'workflow.action_review', - 'ดำเนินการในขั้นตอนปัจจุบัน (เช่น ตรวจสอบแล้ว)' - ), - ( - 46, - 'workflow.force_proceed', - 'บังคับไปยังขั้นตอนถัดไป (Document Control Power)' - ), - ( - 47, - 'workflow.revert', - 'ย้อนกลับไปยังขั้นตอนก่อนหน้า (Document Control Power)' - ); + 45, + 'workflow.action_review', + 'ดำเนินการในขั้นตอนปัจจุบัน (เช่น ตรวจสอบแล้ว)' + ), + ( + 46, + 'workflow.force_proceed', + 'บังคับไปยังขั้นตอนถัดไป (Document Control Power)' + ), + ( + 47, + 'workflow.revert', + 'ย้อนกลับไปยังขั้นตอนก่อนหน้า (Document Control Power)' + ); + -- ===================================================== -- == 5. สิทธิ์ด้านการค้นหาและรายงาน (Search & Reporting) == -- ===================================================== INSERT INTO permissions ( - permission_id, - permission_name, - description - ) + permission_id, + permission_name, + description + ) VALUES ( - 48, - 'search.advanced', - 'ใช้งานการค้นหาขั้นสูง' - ), - ( - 49, - 'report.generate', - 'สร้างรายงานสรุป (รายวัน / สัปดาห์ / เดือน / ปี)' - ); + 48, + 'search.advanced', + 'ใช้งานการค้นหาขั้นสูง' + ), + ( + 49, + 'report.generate', + 'สร้างรายงานสรุป (รายวัน / สัปดาห์ / เดือน / ปี)' + ); + -- ========================================================== -- Seed Role-Permissions Mapping (จับคู่สิทธิ์เริ่มต้น) -- ========================================================== @@ -543,1233 +624,1225 @@ VALUES ( -- This is a robust way to ensure Superadmin always has full power. INSERT INTO role_permissions (role_id, permission_id) SELECT 1, - permission_id + permission_id FROM permissions; + -- ===================================================== -- == 2. Org Admin (role_id = 2) == -- ===================================================== INSERT INTO role_permissions (role_id, permission_id) VALUES -- จัดการผู้ใช้ในองค์กร - (2, 18), - -- user.create - (2, 19), - -- user.edit - (2, 20), - -- user.delete - (2, 21), - -- user.view - (2, 22), - -- user.assign_organization - -- จัดการองค์กร - (2, 3), - -- organization.edit - (2, 5), - -- organization.view - -- จัดการข้อมูลหลักที่อนุญาต (เฉพาะ Tags) - (2, 17), - -- master_data.tag.manage - -- ดูข้อมูลต่างๆ ในองค์กร - (2, 31), - -- document.view - (2, 9), - -- project.view - (2, 28), - -- contract.view - -- การค้นหาและรายงาน - (2, 48), - -- search.advanced - (2, 49); + (2, 18), + -- user.create + (2, 19), + -- user.edit + (2, 20), + -- user.delete + (2, 21), + -- user.view + (2, 22), + -- user.assign_organization + -- จัดการองค์กร + (2, 3), + -- organization.edit + (2, 5), + -- organization.view + -- จัดการข้อมูลหลักที่อนุญาต (เฉพาะ Tags) + (2, 17), + -- master_data.tag.manage + -- ดูข้อมูลต่างๆ ในองค์กร + (2, 31), + -- document.view + (2, 9), + -- project.view + (2, 28), + -- contract.view + -- การค้นหาและรายงาน + (2, 48), + -- search.advanced + (2, 49); + -- report.generate -- ===================================================== -- == 3. Document Control (role_id = 3) == -- ===================================================== INSERT INTO role_permissions (role_id, permission_id) VALUES -- สิทธิ์จัดการเอกสารทั้งหมด - (3, 29), - -- document.create_draft - (3, 30), - -- document.submit - (3, 31), - -- document.view - (3, 32), - -- document.edit - (3, 33), - -- document.admin_edit - (3, 34), - -- document.delete - (3, 35), - -- document.attach - -- สิทธิ์สร้างเอกสารแต่ละประเภท - (3, 36), - -- correspondence.create - (3, 37), - -- rfa.create - (3, 39), - -- drawing.create - (3, 40), - -- transmittal.create - (3, 41), - -- circulation.create - -- สิทธิ์จัดการ Workflow - (3, 45), - -- workflow.action_review - (3, 46), - -- workflow.force_proceed - (3, 47), - -- workflow.revert - -- สิทธิ์จัดการ Circulation - (3, 42), - -- circulation.respond - (3, 43), - -- circulation.acknowledge - (3, 44), - -- circulation.close - -- สิทธิ์อื่นๆ ที่จำเป็น - (3, 38), - -- rfa.manage_shop_drawings - (3, 48), - -- search.advanced - (3, 49); + (3, 29), + -- document.create_draft + (3, 30), + -- document.submit + (3, 31), + -- document.view + (3, 32), + -- document.edit + (3, 33), + -- document.admin_edit + (3, 34), + -- document.delete + (3, 35), + -- document.attach + -- สิทธิ์สร้างเอกสารแต่ละประเภท + (3, 36), + -- correspondence.create + (3, 37), + -- rfa.create + (3, 39), + -- drawing.create + (3, 40), + -- transmittal.create + (3, 41), + -- circulation.create + -- สิทธิ์จัดการ Workflow + (3, 45), + -- workflow.action_review + (3, 46), + -- workflow.force_proceed + (3, 47), + -- workflow.revert + -- สิทธิ์จัดการ Circulation + (3, 42), + -- circulation.respond + (3, 43), + -- circulation.acknowledge + (3, 44), + -- circulation.close + -- สิทธิ์อื่นๆ ที่จำเป็น + (3, 38), + -- rfa.manage_shop_drawings + (3, 48), + -- search.advanced + (3, 49); + -- report.generate -- ===================================================== -- == 4. Editor (role_id = 4) == -- ===================================================== INSERT INTO role_permissions (role_id, permission_id) VALUES -- สิทธิ์แก้ไขเอกสาร (แต่ไม่ใช่สิทธิ์ Admin) - (4, 29), - -- document.create_draft - (4, 30), - -- document.submit - (4, 31), - -- document.view - (4, 32), - -- document.edit - (4, 35), - -- document.attach - -- สิทธิ์สร้างเอกสารแต่ละประเภท - (4, 36), - -- correspondence.create - (4, 37), - -- rfa.create - (4, 39), - -- drawing.create - (4, 40), - -- transmittal.create - (4, 41), - -- circulation.create - -- สิทธิ์อื่นๆ ที่จำเป็น - (4, 38), - -- rfa.manage_shop_drawings - (4, 48); + (4, 29), + -- document.create_draft + (4, 30), + -- document.submit + (4, 31), + -- document.view + (4, 32), + -- document.edit + (4, 35), + -- document.attach + -- สิทธิ์สร้างเอกสารแต่ละประเภท + (4, 36), + -- correspondence.create + (4, 37), + -- rfa.create + (4, 39), + -- drawing.create + (4, 40), + -- transmittal.create + (4, 41), + -- circulation.create + -- สิทธิ์อื่นๆ ที่จำเป็น + (4, 38), + -- rfa.manage_shop_drawings + (4, 48); + -- search.advanced -- ===================================================== -- == 5. Viewer (role_id = 5) == -- ===================================================== INSERT INTO role_permissions (role_id, permission_id) VALUES -- สิทธิ์ดูเท่านั้น - (5, 31), - -- document.view - (5, 48); + (5, 31), + -- document.view + (5, 48); + -- search.advanced -- ===================================================== -- == 6. Project Manager (role_id = 6) == -- ===================================================== INSERT INTO role_permissions (role_id, permission_id) VALUES -- สิทธิ์จัดการโครงการ - (6, 23), - -- project.manage_members - (6, 24), - -- project.create_contracts - (6, 25), - -- project.manage_contracts - (6, 26), - -- project.view_reports - (6, 9), - -- project.view - -- สิทธิ์จัดการข้อมูลหลักระดับโครงการ - (6, 16), - -- master_data.drawing_category.manage - -- สิทธิ์ดูข้อมูลในสัญญา - (6, 28), - -- contract.view - -- สิทธิ์ในการจัดการเอกสาร (ระดับ Editor) - (6, 29), - -- document.create_draft - (6, 30), - -- document.submit - (6, 31), - -- document.view - (6, 32), - -- document.edit - (6, 35), - -- document.attach - (6, 36), - -- correspondence.create - (6, 37), - -- rfa.create - (6, 39), - -- drawing.create - (6, 40), - -- transmittal.create - (6, 41), - -- circulation.create - (6, 38), - -- rfa.manage_shop_drawings - (6, 48), - -- search.advanced - (6, 49); + (6, 23), + -- project.manage_members + (6, 24), + -- project.create_contracts + (6, 25), + -- project.manage_contracts + (6, 26), + -- project.view_reports + (6, 9), + -- project.view + -- สิทธิ์จัดการข้อมูลหลักระดับโครงการ + (6, 16), + -- master_data.drawing_category.manage + -- สิทธิ์ดูข้อมูลในสัญญา + (6, 28), + -- contract.view + -- สิทธิ์ในการจัดการเอกสาร (ระดับ Editor) + (6, 29), + -- document.create_draft + (6, 30), + -- document.submit + (6, 31), + -- document.view + (6, 32), + -- document.edit + (6, 35), + -- document.attach + (6, 36), + -- correspondence.create + (6, 37), + -- rfa.create + (6, 39), + -- drawing.create + (6, 40), + -- transmittal.create + (6, 41), + -- circulation.create + (6, 38), + -- rfa.manage_shop_drawings + (6, 48), + -- search.advanced + (6, 49); + -- report.generate -- ===================================================== -- == 7. Contract Admin (role_id = 7) == -- ===================================================== INSERT INTO role_permissions (role_id, permission_id) VALUES -- สิทธิ์จัดการสัญญา - (7, 27), - -- contract.manage_members - (7, 28), - -- contract.view - -- สิทธิ์ในการอนุมัติ (ส่วนหนึ่งของ Workflow) - (7, 45), - -- workflow.action_review - -- สิทธิ์จัดการข้อมูลเฉพาะสัญญา - (7, 38), - -- rfa.manage_shop_drawings - (7, 39), - -- drawing.create - -- สิทธิ์ในการจัดการเอกสาร (ระดับ Editor) - (7, 29), - -- document.create_draft - (7, 30), - -- document.submit - (7, 31), - -- document.view - (7, 32), - -- document.edit - (7, 35), - -- document.attach - (7, 36), - -- correspondence.create - (7, 37), - -- rfa.create - (7, 40), - -- transmittal.create - (7, 41), - -- circulation.create - (7, 48); + (7, 27), + -- contract.manage_members + (7, 28), + -- contract.view + -- สิทธิ์ในการอนุมัติ (ส่วนหนึ่งของ Workflow) + (7, 45), + -- workflow.action_review + -- สิทธิ์จัดการข้อมูลเฉพาะสัญญา + (7, 38), + -- rfa.manage_shop_drawings + (7, 39), + -- drawing.create + -- สิทธิ์ในการจัดการเอกสาร (ระดับ Editor) + (7, 29), + -- document.create_draft + (7, 30), + -- document.submit + (7, 31), + -- document.view + (7, 32), + -- document.edit + (7, 35), + -- document.attach + (7, 36), + -- correspondence.create + (7, 37), + -- rfa.create + (7, 40), + -- transmittal.create + (7, 41), + -- circulation.create + (7, 48); + -- Seed data for the 'user_assignments' table INSERT INTO `user_assignments` ( - `id`, - `user_id`, - `role_id`, - `organization_id`, - `project_id`, - `contract_id`, - `assigned_by_user_id` - ) + `id`, + `user_id`, + `role_id`, + `organization_id`, + `project_id`, + `contract_id`, + `assigned_by_user_id` + ) VALUES ( - 1, - 1, - 1, - NULL, - NULL, - NULL, - NULL - ), - (2, 2, 2, 1, NULL, NULL, NULL); + 1, + 1, + 1, + NULL, + NULL, + NULL, + NULL + ), + (2, 2, 2, 1, NULL, NULL, NULL); + -- ===================================================== -- == 4. การเชื่อมโยงโครงการกับองค์กร (project_organizations) == -- ===================================================== -- โครงการหลัก (LCBP3) จะมีองค์กรหลักๆ เข้ามาเกี่ยวข้องทั้งหมด INSERT INTO project_organizations (project_id, organization_id) SELECT ( - SELECT id - FROM projects - WHERE project_code = 'LCBP3 ' - ), - id + SELECT id + FROM projects + WHERE project_code = 'LCBP3' + ), + id FROM organizations WHERE organization_code IN ( - 'กทท.', - 'สคฉ.3', - 'TEAM', - 'คคง.', - 'ผรม.1', - 'ผรม.2', - 'ผรม.3', - 'ผรม.4', - 'EN', - 'CAR ' - ); + 'กทท.', + 'สคฉ.3', + 'TEAM', + 'คคง.', + 'ผรม.1', + 'ผรม.2', + 'ผรม.3', + 'ผรม.4', + 'EN', + 'CAR' + ); + -- โครงการย่อย (LCBP3C1) จะมีเฉพาะองค์กรที่เกี่ยวข้อง INSERT INTO project_organizations (project_id, organization_id) SELECT ( - SELECT id - FROM projects - WHERE project_code = 'LCBP3-C1 ' - ), - id + SELECT id + FROM projects + WHERE project_code = 'LCBP3-C1' + ), + id FROM organizations WHERE organization_code IN ( - 'กทท.', - 'สคฉ.3', - 'สคฉ.3 -02', - 'คคง.', - 'ผรม.1 ' - ); + 'กทท.', + 'สคฉ.3', + 'สคฉ.3 -02', + 'คคง.', + 'ผรม.1 ' + ); + -- ทำเช่นเดียวกันสำหรับโครงการอื่นๆ (ตัวอย่าง) INSERT INTO project_organizations (project_id, organization_id) SELECT ( - SELECT id - FROM projects - WHERE project_code = 'LCBP3-C2 ' - ), - id + SELECT id + FROM projects + WHERE project_code = 'LCBP3-C2' + ), + id FROM organizations WHERE organization_code IN ( - 'กทท.', - 'สคฉ.3', - 'สคฉ.3 -03', - 'คคง.', - 'ผรม.2 ' - ); + 'กทท.', + 'สคฉ.3', + 'สคฉ.3 -03', + 'คคง.', + 'ผรม.2' + ); + -- ===================================================== -- == 5. การเชื่อมโยงสัญญากับองค์กร (contract_organizations) == -- ===================================================== -- สัญญาที่ปรึกษาออกแบบ (DSLCBP3) INSERT INTO contract_organizations ( - contract_id, - organization_id, - role_in_contract - ) + contract_id, + organization_id, + role_in_contract + ) VALUES ( - ( - SELECT id - FROM contracts - WHERE contract_code = 'LCBP3-DS' - ), - ( - SELECT id - FROM organizations - WHERE organization_code = 'กทท.' - ), - 'Owner' + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-DS' ), ( - ( - SELECT id - FROM contracts - WHERE contract_code = 'LCBP3-DS' - ), - ( - SELECT id - FROM organizations - WHERE organization_code = 'TEAM' - ), - 'Designer' - ); + SELECT id + FROM organizations + WHERE organization_code = 'กทท.' + ), + 'Owner' + ), + ( + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-DS' + ), + ( + SELECT id + FROM organizations + WHERE organization_code = 'TEAM' + ), + 'Designer' + ); + -- สัญญาที่ปรึกษาควบคุมงาน (PSLCBP3) INSERT INTO contract_organizations ( - contract_id, - organization_id, - role_in_contract - ) + contract_id, + organization_id, + role_in_contract + ) VALUES ( - ( - SELECT id - FROM contracts - WHERE contract_code = 'LCBP3-PS' - ), - ( - SELECT id - FROM organizations - WHERE organization_code = 'กทท.' - ), - 'Owner' + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-PS' ), ( - ( - SELECT id - FROM contracts - WHERE contract_code = 'LCBP3-PS' - ), - ( - SELECT id - FROM organizations - WHERE organization_code = 'คคง.' - ), - 'Consultant' - ); + SELECT id + FROM organizations + WHERE organization_code = 'กทท.' + ), + 'Owner' + ), + ( + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-PS' + ), + ( + SELECT id + FROM organizations + WHERE organization_code = 'คคง.' + ), + 'Consultant' + ); + -- สัญญางานก่อสร้าง ส่วนที่ 1 (LCBP3-C1) INSERT INTO contract_organizations ( - contract_id, - organization_id, - role_in_contract - ) + contract_id, + organization_id, + role_in_contract + ) VALUES ( - ( - SELECT id - FROM contracts - WHERE contract_code = 'LCBP3-C1' - ), - ( - SELECT id - FROM organizations - WHERE organization_code = 'กทท.' - ), - 'Owner' + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-C1' ), ( - ( - SELECT id - FROM contracts - WHERE contract_code = 'LCBP3-C1' - ), - ( - SELECT id - FROM organizations - WHERE organization_code = 'ผรม.1' - ), - 'Contractor' - ); + SELECT id + FROM organizations + WHERE organization_code = 'กทท.' + ), + 'Owner' + ), + ( + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-C1' + ), + ( + SELECT id + FROM organizations + WHERE organization_code = 'ผรม.1' + ), + 'Contractor' + ); + -- สัญญางานก่อสร้าง ส่วนที่ 2 (LCBP3-C2) INSERT INTO contract_organizations ( - contract_id, - organization_id, - role_in_contract - ) + contract_id, + organization_id, + role_in_contract + ) VALUES ( - ( - SELECT id - FROM contracts - WHERE contract_code = 'LCBP3-C2' - ), - ( - SELECT id - FROM organizations - WHERE organization_code = 'กทท.' - ), - 'Owner' + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-C2' ), ( - ( - SELECT id - FROM contracts - WHERE contract_code = 'LCBP3-C2' - ), - ( - SELECT id - FROM organizations - WHERE organization_code = 'ผรม.2' - ), - 'Contractor' - ); + SELECT id + FROM organizations + WHERE organization_code = 'กทท.' + ), + 'Owner' + ), + ( + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-C2' + ), + ( + SELECT id + FROM organizations + WHERE organization_code = 'ผรม.2' + ), + 'Contractor' + ); + -- สัญญาตรวจสอบสิ่งแวดล้อม (LCBP3-EN) INSERT INTO contract_organizations ( - contract_id, - organization_id, - role_in_contract - ) + contract_id, + organization_id, + role_in_contract + ) VALUES ( - ( - SELECT id - FROM contracts - WHERE contract_code = 'LCBP3-EN' - ), - ( - SELECT id - FROM organizations - WHERE organization_code = 'กทท.' - ), - 'Owner' + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-EN' ), ( - ( - SELECT id - FROM contracts - WHERE contract_code = 'LCBP3-EN' - ), - ( - SELECT id - FROM organizations - WHERE organization_code = 'EN' - ), - 'Consultant' - ); + SELECT id + FROM organizations + WHERE organization_code = 'กทท.' + ), + 'Owner' + ), + ( + ( + SELECT id + FROM contracts + WHERE contract_code = 'LCBP3-EN' + ), + ( + SELECT id + FROM organizations + WHERE organization_code = 'EN' + ), + 'Consultant' + ); + -- Seed correspondence_status INSERT INTO correspondence_status ( - status_code, - status_name, - sort_order, - is_active - ) + status_code, + status_name, + sort_order, + is_active + ) VALUES ('DRAFT', 'Draft', 10, 1), - ( - 'SUBOWN', - 'Submitted to Owner', - 21, - 1 - ), - ( - 'SUBDSN', - 'Submitted to Designer', - 22, - 1 - ), - ( - 'SUBCSC', - 'Submitted to CSC', - 23, - 1 - ), - ( - 'SUBCON', - 'Submitted to Contractor', - 24, - 1 - ), - ( - 'SUBOTH', - 'Submitted to Others', - 25, - 1 - ), - ( - 'REPOWN', - 'Reply by Owner', - 31, - 1 - ), - ( - 'REPDSN', - 'Reply by Designer', - 32, - 1 - ), - ( - 'REPCSC', - 'Reply by CSC', - 33, - 1 - ), - ( - 'REPCON', - 'Reply by Contractor', - 34, - 1 - ), - ( - 'REPOTH', - 'Reply by Others', - 35, - 1 - ), - ( - 'RSBOWN', - 'Resubmited by Owner', - 41, - 1 - ), - ( - 'RSBDSN', - 'Resubmited by Designer', - 42, - 1 - ), - ( - 'RSBCSC', - 'Resubmited by CSC', - 43, - 1 - ), - ( - 'RSBCON', - 'Resubmited by Contractor', - 44, - 1 - ), - ( - 'CLBOWN', - 'Closed by Owner', - 51, - 1 - ), - ( - 'CLBDSN', - 'Closed by Designer', - 52, - 1 - ), - ( - 'CLBCSC', - 'Closed by CSC', - 53, - 1 - ), - ( - 'CLBCON', - 'Closed by Contractor', - 54, - 1 - ), - ( - 'CCBOWN', - 'Canceled by Owner', - 91, - 1 - ), - ( - 'CCBDSN', - 'Canceled by Designer', - 92, - 1 - ), - ( - 'CCBCSC', - 'Canceled by CSC', - 93, - 1 - ), - ( - 'CCBCON', - 'Canceled by Contractor', - 94, - 1 - ); + ( + 'SUBOWN', + 'Submitted to Owner', + 21, + 1 + ), + ( + 'SUBDSN', + 'Submitted to Designer', + 22, + 1 + ), + ( + 'SUBCSC', + 'Submitted to CSC', + 23, + 1 + ), + ( + 'SUBCON', + 'Submitted to Contractor', + 24, + 1 + ), + ( + 'SUBOTH', + 'Submitted to Others', + 25, + 1 + ), + ( + 'REPOWN', + 'Reply by Owner', + 31, + 1 + ), + ( + 'REPDSN', + 'Reply by Designer', + 32, + 1 + ), + ('REPCSC', 'Reply by CSC', 33, 1), + ( + 'REPCON', + 'Reply by Contractor', + 34, + 1 + ), + ( + 'REPOTH', + 'Reply by Others', + 35, + 1 + ), + ( + 'RSBOWN', + 'Resubmited by Owner', + 41, + 1 + ), + ( + 'RSBDSN', + 'Resubmited by Designer', + 42, + 1 + ), + ( + 'RSBCSC', + 'Resubmited by CSC', + 43, + 1 + ), + ( + 'RSBCON', + 'Resubmited by Contractor', + 44, + 1 + ), + ( + 'CLBOWN', + 'Closed by Owner', + 51, + 1 + ), + ( + 'CLBDSN', + 'Closed by Designer', + 52, + 1 + ), + ('CLBCSC', 'Closed by CSC', 53, 1), + ( + 'CLBCON', + 'Closed by Contractor', + 54, + 1 + ), + ( + 'CCBOWN', + 'Canceled by Owner', + 91, + 1 + ), + ( + 'CCBDSN', + 'Canceled by Designer', + 92, + 1 + ), + ( + 'CCBCSC', + 'Canceled by CSC', + 93, + 1 + ), + ( + 'CCBCON', + 'Canceled by Contractor', + 94, + 1 + ); + -- Seed correspondence_types INSERT INTO correspondence_types ( - type_code, - type_name, - sort_order, - is_active - ) + type_code, + type_name, + sort_order, + is_active + ) VALUES ( - 'RFA', - 'Request for Approval', - 1, - 1 - ), - ( - 'RFI', - 'Request for Information', - 2, - 1 - ), - ( - 'TRANSMITTAL', - 'Transmittal', - 3, - 1 - ), - ('EMAIL', 'Email', 4, 1), - ( - 'INSTRUCTION', - 'Instruction', - 5, - 1 - ), - ('LETTER', 'Letter', 6, 1), - ('MEMO', 'Memorandum', 7, 1), - ( - 'MOM', - 'Minutes of Meeting', - 8, - 1 - ), - ('NOTICE', 'Notice', 9, 1), - ('OTHER', 'Other', 10, 1); + 'RFA', + 'Request for Approval', + 1, + 1 + ), + ( + 'RFI', + 'Request for Information', + 2, + 1 + ), + ( + 'TRANSMITTAL', + 'Transmittal', + 3, + 1 + ), + ('EMAIL', 'Email', 4, 1), + ( + 'INSTRUCTION', + 'Instruction', + 5, + 1 + ), + ('LETTER', 'Letter', 6, 1), + ('MEMO', 'Memorandum', 7, 1), + ( + 'MOM', + 'Minutes of Meeting', + 8, + 1 + ), + ('NOTICE', 'Notice', 9, 1), + ('OTHER', 'Other', 10, 1); + -- Seed rfa_types INSERT INTO rfa_types ( - contract_id, - type_code, - type_name_en, - type_name_th - ) + contract_id, + type_code, + type_name_en, + type_name_th + ) SELECT id, - 'ADW', - 'As Built Drawing', - 'แบบร่างหลังการก่อสร้าง' + 'ADW', + 'As Built Drawing', + 'แบบร่างหลังการก่อสร้าง' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'BC', - 'Box Culvert', - 'ท่อระบายน้ำรูปกล่อง' + 'BC', + 'Box Culvert', + 'ท่อระบายน้ำรูปกล่อง' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'BM', - 'Benchmark', - 'หมุดหลักฐาน' + 'BM', + 'Benchmark', + 'หมุดหลักฐาน' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'CER', - 'Certificates', - 'ใบรับรอง' + 'CER', + 'Certificates', + 'ใบรับรอง' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'CN', - 'Canal Drainage', - 'ระบบระบายน้ำในคลอง' + 'CN', + 'Canal Drainage', + 'ระบบระบายน้ำในคลอง' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'CON', - 'Contract', - 'สัญญา' + 'CON', + 'Contract', + 'สัญญา' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'DDS', - 'Design Data Submission', - 'นำส่งข้อมูลการออกแบบ' + 'DDS', + 'Design Data Submission', + 'นำส่งข้อมูลการออกแบบ' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'DDW', - 'Draft Drawing', - 'แบบร่าง' + 'DDW', + 'Draft Drawing', + 'แบบร่าง' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'DRW', - 'Drawings (All Types)', - 'แบบก่อสร้าง' + 'DRW', + 'Drawings (All Types)', + 'แบบก่อสร้าง' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'DSN', - 'Design/Calculation/Manual (All Stages)', - 'ออกแบบ / คำนวณ / คู่มือ' + 'DSN', + 'Design/Calculation/Manual (All Stages)', + 'ออกแบบ / คำนวณ / คู่มือ' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'GEN', - 'General', - 'ทั่วไป' + 'GEN', + 'General', + 'ทั่วไป' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'ICR', - 'Incident Report', - 'รายงานการเกิดอุบัติเหตุและการบาดเจ็บ' + 'ICR', + 'Incident Report', + 'รายงานการเกิดอุบัติเหตุและการบาดเจ็บ' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'INS', - 'Insurances/Bond/Guarantee', - 'การประกัน / พันธบัตร / การค้ำประกัน' + 'INS', + 'Insurances/Bond/Guarantee', + 'การประกัน / พันธบัตร / การค้ำประกัน' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'INR', - 'Inspection/Audit/Surveillance Report', - 'รายงานการตรวจสอบ / การตรวจสอบ / รายงานการเฝ้าระวัง' + 'INR', + 'Inspection/Audit/Surveillance Report', + 'รายงานการตรวจสอบ / การตรวจสอบ / รายงานการเฝ้าระวัง' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'ITP', - 'Inspection and Test Plan', - 'แผนการตรวจสอบและทดสอบ' + 'ITP', + 'Inspection and Test Plan', + 'แผนการตรวจสอบและทดสอบ' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'JSA', - 'Jobs Analysis', - 'รายงานการวิเคราะห์ความปลอดภัย' + 'JSA', + 'Jobs Analysis', + 'รายงานการวิเคราะห์ความปลอดภัย' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'MAN', - 'Manual', - 'คู่มือ' + 'MAN', + 'Manual', + 'คู่มือ' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'MAT', - 'Materials/Equipment/Plant', - 'วัสดุ / อุปกรณ์ / โรงงาน' + 'MAT', + 'Materials/Equipment/Plant', + 'วัสดุ / อุปกรณ์ / โรงงาน' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'MOM', - 'Minutes of Meeting', - 'รายงานการประชุม' + 'MOM', + 'Minutes of Meeting', + 'รายงานการประชุม' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'MPR', - 'Monthly Progress Report', - 'รายงานความคืบหน้าประจำเดือน' + 'MPR', + 'Monthly Progress Report', + 'รายงานความคืบหน้าประจำเดือน' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'MST', - 'Method Statement for Construction/Installation', - 'ขั้นตอนการก่อสร้าง / ติดตั้ง' + 'MST', + 'Method Statement for Construction/Installation', + 'ขั้นตอนการก่อสร้าง / ติดตั้ง' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'NDS', - 'Non-Design Data Submission', - 'นำส่งข้อมูลที่ไม่เกี่ยวข้องกับการออกแบบ' + 'NDS', + 'Non-Design Data Submission', + 'นำส่งข้อมูลที่ไม่เกี่ยวข้องกับการออกแบบ' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'PMA', - 'Payment/Invoice/Retention/Estimate', - 'การชำระเงิน / ใบแจ้งหนี้ / ประกันผลงาน / ประมาณการ' + 'PMA', + 'Payment/Invoice/Retention/Estimate', + 'การชำระเงิน / ใบแจ้งหนี้ / ประกันผลงาน / ประมาณการ' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'PRD', - 'Procedure', - 'ระเบียบปฏิบัติ' + 'PRD', + 'Procedure', + 'ระเบียบปฏิบัติ' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'PRG', - 'Progress of Construction', - 'ความคืบหน้าของการก่อสร้าง / ภาพถ่าย / วิดีโอ' + 'PRG', + 'Progress of Construction', + 'ความคืบหน้าของการก่อสร้าง / ภาพถ่าย / วิดีโอ' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'QMS', - 'Quality Document (Plan/Work Instruction)', - 'เอกสารด้านคุณภาพ (แผนงาน / ข้อแนะนำในการทำงาน)' + 'QMS', + 'Quality Document (Plan/Work Instruction)', + 'เอกสารด้านคุณภาพ (แผนงาน / ข้อแนะนำในการทำงาน)' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'RPT', - 'Report', - 'รายงาน' + 'RPT', + 'Report', + 'รายงาน' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'SAR', - 'Semi Annual Report', - 'รายงานประจำหกเดือน' + 'SAR', + 'Semi Annual Report', + 'รายงานประจำหกเดือน' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'SCH', - 'Schedule and Program', - 'แผนงาน' + 'SCH', + 'Schedule and Program', + 'แผนงาน' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'SDW', - 'Shop Drawing', - 'แบบขยายรายละเอียด' + 'SDW', + 'Shop Drawing', + 'แบบขยายรายละเอียด' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'SI', - 'Soil Investigation', - 'การตรวจสอบดิน' + 'SI', + 'Soil Investigation', + 'การตรวจสอบดิน' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'SPE', - 'Specification', - 'ข้อกำหนด' + 'SPE', + 'Specification', + 'ข้อกำหนด' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'TNR', - 'Training Report', - 'รายงานการฝึกปฏิบัติ' + 'TNR', + 'Training Report', + 'รายงานการฝึกปฏิบัติ' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'UC', - 'Underground Construction', - 'โครงสร้างใต้ดิน' + 'UC', + 'Underground Construction', + 'โครงสร้างใต้ดิน' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'VEN', - 'Vendor', - 'ผู้ขาย' + 'VEN', + 'Vendor', + 'ผู้ขาย' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'VRO', - 'Variation Request/Instruction/Order', - 'คำขอเปลี่ยนแปลง / ข้อเสนอแนะ / ข้อเรียกร้อง' + 'VRO', + 'Variation Request/Instruction/Order', + 'คำขอเปลี่ยนแปลง / ข้อเสนอแนะ / ข้อเรียกร้อง' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'WTY', - 'Warranty', - 'การประกัน' + 'WTY', + 'Warranty', + 'การประกัน' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'GEN', - 'General', - 'ทั่วไป' + 'GEN', + 'General', + 'ทั่วไป' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'CON', - 'Contract', - 'สัญญา' + 'CON', + 'Contract', + 'สัญญา' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'INS', - 'Insurances/Bond/Guarantee', - 'การประกัน / พันธบัตร / การค้ำประกัน' + 'INS', + 'Insurances/Bond/Guarantee', + 'การประกัน / พันธบัตร / การค้ำประกัน' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'SCH', - 'Schedule and Program', - 'แผนงาน' + 'SCH', + 'Schedule and Program', + 'แผนงาน' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'PMA', - 'Payment/Invoice/Retention/Estimate', - 'การชำระเงิน / ใบแจ้งหนี้ / ประกันผลงาน / ประมาณการ' + 'PMA', + 'Payment/Invoice/Retention/Estimate', + 'การชำระเงิน / ใบแจ้งหนี้ / ประกันผลงาน / ประมาณการ' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'VRO', - 'Variation Request/Instruction/Order', - 'คำขอเปลี่ยนแปลง / ข้อเสนอแนะ / ข้อเรียกร้อง' + 'VRO', + 'Variation Request/Instruction/Order', + 'คำขอเปลี่ยนแปลง / ข้อเสนอแนะ / ข้อเรียกร้อง' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'VEN', - 'Vendor', - 'ผู้ขาย' + 'VEN', + 'Vendor', + 'ผู้ขาย' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'WTY', - 'Warranty', - 'การประกัน' + 'WTY', + 'Warranty', + 'การประกัน' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'DRW', - 'Drawings (All Types)', - 'แบบก่อสร้าง' + 'DRW', + 'Drawings (All Types)', + 'แบบก่อสร้าง' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'DDW', - 'Draft Drawing', - 'แบบร่าง' + 'DDW', + 'Draft Drawing', + 'แบบร่าง' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'SDW', - 'Shop Drawing', - 'แบบขยายรายละเอียด' + 'SDW', + 'Shop Drawing', + 'แบบขยายรายละเอียด' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'ADW', - 'As Built Drawing', - 'แบบร่างหลังการก่อสร้าง' + 'ADW', + 'As Built Drawing', + 'แบบร่างหลังการก่อสร้าง' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'DDS', - 'Design Data Submission', - 'นำส่งข้อมูลการออกแบบ' + 'DDS', + 'Design Data Submission', + 'นำส่งข้อมูลการออกแบบ' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'DSN', - 'Design/Calculation/Manual (All Stages)', - 'ออกแบบ / คำนวณ / คู่มือ' + 'DSN', + 'Design/Calculation/Manual (All Stages)', + 'ออกแบบ / คำนวณ / คู่มือ' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'NDS', - 'Non-Design Data Submission', - 'นำส่งข้อมูลที่ไม่เกี่ยวข้องกับการออกแบบ' + 'NDS', + 'Non-Design Data Submission', + 'นำส่งข้อมูลที่ไม่เกี่ยวข้องกับการออกแบบ' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'PRD', - 'Procedure', - 'ระเบียบปฏิบัติ' + 'PRD', + 'Procedure', + 'ระเบียบปฏิบัติ' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'MST', - 'Method Statement for Construction/Installation', - 'ขั้นตอนการก่อสร้าง / ติดตั้ง' + 'MST', + 'Method Statement for Construction/Installation', + 'ขั้นตอนการก่อสร้าง / ติดตั้ง' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'QMS', - 'Quality Document (Plan/Work Instruction)', - 'เอกสารด้านคุณภาพ (แผนงาน / ข้อแนะนำในการทำงาน)' + 'QMS', + 'Quality Document (Plan/Work Instruction)', + 'เอกสารด้านคุณภาพ (แผนงาน / ข้อแนะนำในการทำงาน)' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'INR', - 'Inspection/Audit/Surveillance Report', - 'รายงานการตรวจสอบ / การตรวจสอบ / รายงานการเฝ้าระวัง' + 'INR', + 'Inspection/Audit/Surveillance Report', + 'รายงานการตรวจสอบ / การตรวจสอบ / รายงานการเฝ้าระวัง' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'ITP', - 'Inspection and Test Plan', - 'แผนการตรวจสอบและทดสอบ' + 'ITP', + 'Inspection and Test Plan', + 'แผนการตรวจสอบและทดสอบ' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'MAT', - 'Materials/Equipment/Plant', - 'วัสดุ / อุปกรณ์ / โรงงาน' + 'MAT', + 'Materials/Equipment/Plant', + 'วัสดุ / อุปกรณ์ / โรงงาน' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'SPE', - 'Specification', - 'ข้อกำหนด' + 'SPE', + 'Specification', + 'ข้อกำหนด' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'MAN', - 'Manual', - 'คู่มือ' + 'MAN', + 'Manual', + 'คู่มือ' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'CER', - 'Certificates', - 'ใบรับรอง' + 'CER', + 'Certificates', + 'ใบรับรอง' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'SAR', - 'Semi Annual Report', - 'รายงานประจำหกเดือน' + 'SAR', + 'Semi Annual Report', + 'รายงานประจำหกเดือน' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'JSA', - 'Jobs Analysis', - 'รายงานการวิเคราะห์ความปลอดภัย' + 'JSA', + 'Jobs Analysis', + 'รายงานการวิเคราะห์ความปลอดภัย' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'MOM', - 'Minutes of Meeting', - 'รายงานการประชุม' + 'MOM', + 'Minutes of Meeting', + 'รายงานการประชุม' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'MPR', - 'Monthly Progress Report', - 'รายงานความคืบหน้าประจำเดือน' + 'MPR', + 'Monthly Progress Report', + 'รายงานความคืบหน้าประจำเดือน' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'ICR', - 'Incident Report', - 'รายงานการเกิดอุบัติเหตุและการบาดเจ็บ' + 'ICR', + 'Incident Report', + 'รายงานการเกิดอุบัติเหตุและการบาดเจ็บ' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'PRG', - 'Progress of Construction', - 'ความคืบหน้าของการก่อสร้าง / ภาพถ่าย / วิดีโอ' + 'PRG', + 'Progress of Construction', + 'ความคืบหน้าของการก่อสร้าง / ภาพถ่าย / วิดีโอ' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'RPT', - 'Report', - 'รายงาน' + 'RPT', + 'Report', + 'รายงาน' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'TNR', - 'Training Report', - 'รายงานการฝึกปฏิบัติ' + 'TNR', + 'Training Report', + 'รายงานการฝึกปฏิบัติ' FROM contracts WHERE contract_code = 'LCBP3-C2'; + -- Seed rfa_status_codes INSERT INTO rfa_status_codes ( - status_code, - status_name, - description, - sort_order - ) + status_code, + status_name, + description, + sort_order + ) VALUES ('DFT', 'Draft', 'ฉบับร่าง', 1), - ( - 'FAP', - 'For Approve', - 'เพื่อขออนุมัติ', - 11 - ), - ( - 'FRE', - 'For Review', - 'เพื่อตรวจสอบ', - 12 - ), - ( - 'FCO', - 'For Construction', - 'เพื่อก่อสร้าง', - 20 - ), - ( - 'ASB', - 'AS - Built', - 'แบบก่อสร้างจริง', - 30 - ), - ( - 'OBS', - 'Obsolete', - 'ไม่ใช้งาน', - 80 - ), - ( - 'CC', - 'Canceled', - 'ยกเลิก', - 99 - ); + ( + 'FAP', + 'For Approve', + 'เพื่อขออนุมัติ', + 11 + ), + ( + 'FRE', + 'For Review', + 'เพื่อตรวจสอบ', + 12 + ), + ( + 'FCO', + 'For Construction', + 'เพื่อก่อสร้าง', + 20 + ), + ( + 'ASB', + 'AS - Built', + 'แบบก่อสร้างจริง', + 30 + ), + ( + 'OBS', + 'Obsolete', + 'ไม่ใช้งาน', + 80 + ), + ('CC', 'Canceled', 'ยกเลิก', 99); + INSERT INTO rfa_approve_codes ( - approve_code, - approve_name, - sort_order, - is_active - ) + approve_code, + approve_name, + sort_order, + is_active + ) VALUES ( - '1A', - 'Approved by Authority', - 10, - 1 - ), - ( - '1C', - 'Approved by CSC', - 11, - 1 - ), - ( - '1N', - 'Approved As Note', - 12, - 1 - ), - ( - '1R', - 'Approved with Remarks', - 13, - 1 - ), - ( - '3C', - 'Consultant Comments', - 31, - 1 - ), - ( - '3R', - 'Revise + '1A', + 'Approved by Authority', + 10, + 1 + ), + ('1C', 'Approved by CSC', 11, 1), + ('1N', 'Approved As Note', 12, 1), + ( + '1R', + 'Approved with Remarks', + 13, + 1 + ), + ( + '3C', + 'Consultant Comments', + 31, + 1 + ), + ( + '3R', + 'Revise and Resubmit', - 32, - 1 - ), - ('4X', 'Reject', 40, 1), - ( - '5N', - 'No Further Action', - 50, - 1 - ); + 32, + 1 + ), + ('4X', 'Reject', 40, 1), + ('5N', 'No Further Action', 50, 1); + -- Seed circulation_status_codes INSERT INTO circulation_status_codes (code, description, sort_order) VALUES ('OPEN', 'Open', 1), - ('IN_REVIEW', 'In Review', 2), - ('COMPLETED', 'ปCompleted', 3), - ( - 'CANCELLED', - 'Cancelled / Withdrawn', - 9 - ); + ('IN_REVIEW', 'In Review', 2), + ('COMPLETED', 'ปCompleted', 3), + ( + 'CANCELLED', + 'Cancelled / Withdrawn', + 9 + ); + -- ตาราง "แม่" ของ RFA (มีความสัมพันธ์ 1:N กับ rfa_revisions) -- ========================================================== -- SEED DATA 6B.md (Disciplines, RFA Types, Sub Types) @@ -1777,693 +1850,699 @@ VALUES ('OPEN', 'Open', 1), -- 1. Seed ข้อมูล Disciplines (สาขางาน) -- LCBP3-C1 INSERT INTO disciplines ( - contract_id, - discipline_code, - code_name_th, - code_name_en - ) + contract_id, + discipline_code, + code_name_th, + code_name_en + ) SELECT id, - 'GEN', - 'งานบริหารโครงการ', - 'General Management' + 'GEN', + 'งานบริหารโครงการ', + 'General Management' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'COD', - 'สัญญาและข้อโต้แย้ง', - 'Contracting' + 'COD', + 'สัญญาและข้อโต้แย้ง', + 'Contracting' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'QSB', - 'สำรวจปริมาณและควบคุมงบประมาณ', - 'Quantity Survey and Budget Control' + 'QSB', + 'สำรวจปริมาณและควบคุมงบประมาณ', + 'Quantity Survey and Budget Control' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'PPG', - 'บริหารแผนและความก้าวหน้า', - 'Plan and Progress Management' + 'PPG', + 'บริหารแผนและความก้าวหน้า', + 'Plan and Progress Management' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'PRC', - 'งานจัดซื้อ', - 'Procurement' + 'PRC', + 'งานจัดซื้อ', + 'Procurement' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'SUB', - 'ผู้รับเหมาช่วง', - 'Subcontractor' + 'SUB', + 'ผู้รับเหมาช่วง', + 'Subcontractor' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'ODC', - 'สำนักงาน-ควบคุมเอกสาร', - 'Operation Docment Control' + 'ODC', + 'สำนักงาน-ควบคุมเอกสาร', + 'Operation Docment Control' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'LAW', - 'กฎหมาย', - 'Law' + 'LAW', + 'กฎหมาย', + 'Law' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'TRF', - 'จราจร', - 'Traffic' + 'TRF', + 'จราจร', + 'Traffic' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'BIM', - 'BIM', - 'Building information modeling' + 'BIM', + 'BIM', + 'Building information modeling' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'SRV', - 'งานสำรวจ', - 'Survey' + 'SRV', + 'งานสำรวจ', + 'Survey' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'SFT', - 'ความปลอดภัย', - 'Safety' + 'SFT', + 'ความปลอดภัย', + 'Safety' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'BST', - 'งานโครงสร้างอาคาร', - 'Building Structure Work' + 'BST', + 'งานโครงสร้างอาคาร', + 'Building Structure Work' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'TEM', - 'งานชั่วคราว', - 'Temporary Work' + 'TEM', + 'งานชั่วคราว', + 'Temporary Work' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'UTL', - 'งานระบบสาธารณูปโภค', - 'Utility' + 'UTL', + 'งานระบบสาธารณูปโภค', + 'Utility' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'EPW', - 'งานระบบไฟฟ้า', - 'Electrical Power Work' + 'EPW', + 'งานระบบไฟฟ้า', + 'Electrical Power Work' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'ECM', - 'งานระบบไฟฟ้าสื่อสาร', - 'Electrical Communication Work' + 'ECM', + 'งานระบบไฟฟ้าสื่อสาร', + 'Electrical Communication Work' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'ENV', - 'สิ่งแวดล้อม', - 'Environment' + 'ENV', + 'สิ่งแวดล้อม', + 'Environment' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'AQV', - 'คุณภาพอากาศและความสั่นสะเทือน', - 'Air quality and vibration' + 'AQV', + 'คุณภาพอากาศและความสั่นสะเทือน', + 'Air quality and vibration' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'WAB', - 'คุณภาพน้ำและชีววิทยาทางน้ำ', - 'Water quality and Aquatic biology' + 'WAB', + 'คุณภาพน้ำและชีววิทยาทางน้ำ', + 'Water quality and Aquatic biology' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'ONS', - 'วิศวกรรมชายฝั่ง', - 'Onshore Engineer Work' + 'ONS', + 'วิศวกรรมชายฝั่ง', + 'Onshore Engineer Work' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'PPR', - 'มวลชนสัมพันธ์และการประชาสัมพันธ์', - 'Public Relations' + 'PPR', + 'มวลชนสัมพันธ์และการประชาสัมพันธ์', + 'Public Relations' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'OSW', - 'งานก่อสร้างงานทางทะเล', - 'Offshore Work' + 'OSW', + 'งานก่อสร้างงานทางทะเล', + 'Offshore Work' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'DRE', - 'งานขุดและถมทะเล', - 'Dredging and Reclamation' + 'DRE', + 'งานขุดและถมทะเล', + 'Dredging and Reclamation' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'REV', - 'งานคันหินล้อมพื้นที่ถมทะเล', - 'Revetment' + 'REV', + 'งานคันหินล้อมพื้นที่ถมทะเล', + 'Revetment' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'BRW', - 'งานเขื่อนกันคลื่น', - 'Breakwater' + 'BRW', + 'งานเขื่อนกันคลื่น', + 'Breakwater' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'SOI', - 'ปรับปรุงคุณภาพดิน', - 'Soil Improvement' + 'SOI', + 'ปรับปรุงคุณภาพดิน', + 'Soil Improvement' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'BLC', - 'งานปรับปรุงคลองบางละมุง', - 'Bang Lamung Canal Bank Protection' + 'BLC', + 'งานปรับปรุงคลองบางละมุง', + 'Bang Lamung Canal Bank Protection' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'FUP', - 'งานประตูระบายน้ำและท่อลอด', - 'Floodgate & Under Ground Piping Works' + 'FUP', + 'งานประตูระบายน้ำและท่อลอด', + 'Floodgate & Under Ground Piping Works' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'SWP', - 'งานอาคารควบคุมสถานีสูบน้ำทะเล', - 'Sea Water Pumping Station Control BuilDing' + 'SWP', + 'งานอาคารควบคุมสถานีสูบน้ำทะเล', + 'Sea Water Pumping Station Control BuilDing' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'NAV', - 'งานติดตั้งเครื่องหมายช่วงการเดินเรือ', - 'Navigations Aids' + 'NAV', + 'งานติดตั้งเครื่องหมายช่วงการเดินเรือ', + 'Navigations Aids' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'GEO', - 'งานด้านธรณีเทคนิค', - 'Geotechnical' + 'GEO', + 'งานด้านธรณีเทคนิค', + 'Geotechnical' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'CRW', - 'งานด้านโยธา - Rock Works', - 'Civil-Rock work' + 'CRW', + 'งานด้านโยธา - Rock Works', + 'Civil-Rock work' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'DVR', - 'ทีมนักประดาน้ำ', - 'Dive Work' + 'DVR', + 'ทีมนักประดาน้ำ', + 'Dive Work' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'MTS', - 'งานทดสอบวัสดุและธรณีเทคนิค', - 'Materials and Geotechnical Testing' + 'MTS', + 'งานทดสอบวัสดุและธรณีเทคนิค', + 'Materials and Geotechnical Testing' FROM contracts WHERE contract_code = 'LCBP3-C1' UNION ALL SELECT id, - 'OTH', - 'อื่นๆ', - 'Other' + 'OTH', + 'อื่นๆ', + 'Other' FROM contracts WHERE contract_code = 'LCBP3-C1'; + -- LCBP3-C2 INSERT INTO disciplines ( - contract_id, - discipline_code, - code_name_th, - code_name_en - ) + contract_id, + discipline_code, + code_name_th, + code_name_en + ) SELECT id, - 'GEN', - 'งานบริหารโครงการ', - 'Project Management' + 'GEN', + 'งานบริหารโครงการ', + 'Project Management' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'COD', - 'สัญญาและข้อโต้แย้ง', - 'Contracts and arguments' + 'COD', + 'สัญญาและข้อโต้แย้ง', + 'Contracts and arguments' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'QSB', - 'สำรวจปริมาณและควบคุมงบประมาณ', - 'Survey the quantity and control the budget' + 'QSB', + 'สำรวจปริมาณและควบคุมงบประมาณ', + 'Survey the quantity and control the budget' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'PPM', - 'บริหารแผนและความก้าวหน้า', - 'Plan Management & Progress' + 'PPM', + 'บริหารแผนและความก้าวหน้า', + 'Plan Management & Progress' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'ODC', - 'สำนักงาน-ควบคุมเอกสาร', - 'Document Control Office' + 'ODC', + 'สำนักงาน-ควบคุมเอกสาร', + 'Document Control Office' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'LAW', - 'กฎหมาย', - 'Law' + 'LAW', + 'กฎหมาย', + 'Law' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'TRF', - 'จราจร', - 'Traffic' + 'TRF', + 'จราจร', + 'Traffic' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'BIM', - 'Building Information Modeling', - 'Building Information Modeling' + 'BIM', + 'Building Information Modeling', + 'Building Information Modeling' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'SRV', - 'งานสำรวจ', - 'Survey' + 'SRV', + 'งานสำรวจ', + 'Survey' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'SFT', - 'ความปลอดภัย', - 'Safety' + 'SFT', + 'ความปลอดภัย', + 'Safety' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'BST', - 'งานโครงสร้างอาคาร', - 'Building Structure' + 'BST', + 'งานโครงสร้างอาคาร', + 'Building Structure' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'UTL', - 'งานะบบสาธารณูปโภค', - 'Public Utilities' + 'UTL', + 'งานะบบสาธารณูปโภค', + 'Public Utilities' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'EPW', - 'งานระบบไฟฟ้า', - 'Electrical Systems' + 'EPW', + 'งานระบบไฟฟ้า', + 'Electrical Systems' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'ECM', - 'งานระบบไฟฟ้าสื่อสาร', - 'Electrical Communication System' + 'ECM', + 'งานระบบไฟฟ้าสื่อสาร', + 'Electrical Communication System' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'ENV', - 'สิ่งแวดล้อม', - 'Environment' + 'ENV', + 'สิ่งแวดล้อม', + 'Environment' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'AQV', - 'คุณภาพอากาศและความสั่นสะเทือน', - 'Air Quality and Vibration' + 'AQV', + 'คุณภาพอากาศและความสั่นสะเทือน', + 'Air Quality and Vibration' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'WAB', - 'คุณภาพน้ำและชีววิทยาทางน้ำ', - 'Water Quality and Aquatic Biology' + 'WAB', + 'คุณภาพน้ำและชีววิทยาทางน้ำ', + 'Water Quality and Aquatic Biology' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'ONS', - 'วิศวกรรมชายฝั่ง', - 'Coastal Engineering' + 'ONS', + 'วิศวกรรมชายฝั่ง', + 'Coastal Engineering' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'PPR', - 'มวลชนสัมพันธ์และประชาสัมพันธ์', - 'Mass Relations and Public Relations' + 'PPR', + 'มวลชนสัมพันธ์และประชาสัมพันธ์', + 'Mass Relations and Public Relations' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'OFW', - 'งานก่อสร้างทางทะเล', - 'Marine Construction' + 'OFW', + 'งานก่อสร้างทางทะเล', + 'Marine Construction' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'EXR', - 'งานขุดและถมทะเล', - 'Excavation and reclamation' + 'EXR', + 'งานขุดและถมทะเล', + 'Excavation and reclamation' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'GEO', - 'งานด้านธรณีเทคนิค', - 'Geotechnical work' + 'GEO', + 'งานด้านธรณีเทคนิค', + 'Geotechnical work' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'CRW', - 'งานด้านโยธา - Rock Works', - 'Civil Works - Rock Works' + 'CRW', + 'งานด้านโยธา - Rock Works', + 'Civil Works - Rock Works' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'DVW', - 'ทีมนักประดาน้ำ', - 'Team of Divers' + 'DVW', + 'ทีมนักประดาน้ำ', + 'Team of Divers' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'MTT', - 'งานทดสอบวัสดุ', - 'Materials Testing' + 'MTT', + 'งานทดสอบวัสดุ', + 'Materials Testing' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'ARC', - 'งานสถาปัตยกรรม', - 'Architecture' + 'ARC', + 'งานสถาปัตยกรรม', + 'Architecture' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'STR', - 'งานโครงสร้าง', - 'Structural work' + 'STR', + 'งานโครงสร้าง', + 'Structural work' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'SAN', - 'งานระบบสุขาภิบาล', - 'Sanitation System' + 'SAN', + 'งานระบบสุขาภิบาล', + 'Sanitation System' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'DRA', - 'งานระบบระบายน้ำ', - 'Drainage system work' + 'DRA', + 'งานระบบระบายน้ำ', + 'Drainage system work' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'TER', - 'งานท่าเทียบเรือ', - 'Terminal Work work' + 'TER', + 'งานท่าเทียบเรือ', + 'Terminal Work work' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'BUD', - 'งานอาคาร', - 'Building' + 'BUD', + 'งานอาคาร', + 'Building' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'ROW', - 'งานถนนและสะพาน', - 'Road and Bridge Work' + 'ROW', + 'งานถนนและสะพาน', + 'Road and Bridge Work' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'MEC', - 'งานเคริองกล', - 'Mechanical work' + 'MEC', + 'งานเคริองกล', + 'Mechanical work' FROM contracts WHERE contract_code = 'LCBP3-C2' UNION ALL SELECT id, - 'OTH', - 'อื่น ๆ', - 'Others' + 'OTH', + 'อื่น ๆ', + 'Others' FROM contracts WHERE contract_code = 'LCBP3-C2'; + -- 2. Seed ข้อมูล Correspondence Sub Types (Mapping RFA Types กับ Number) -- เนื่องจาก sub_type_code ตรงกับ RFA Type Code แต่ Req ต้องการ Mapping เป็น Number -- LCBP3-C1 +-- LCBP3-C1 INSERT INTO correspondence_sub_types ( - contract_id, - correspondence_type_id, - sub_type_code, - sub_type_name, - ) + contract_id, + correspondence_type_id, + sub_type_code, + sub_type_name, + sub_type_number + ) SELECT c.id, - ct.id, - 'MAT', - 'Material Approval', - '11' + ct.id, + 'MAT', + 'Material Approval', + '11' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C1' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'SHP', - 'Shop Drawing Submittal', - '12' + ct.id, + 'SHP', + 'Shop Drawing Submittal', + '12' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C1' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'DWG', - 'Document Approval', - '13' + ct.id, + 'DWG', + 'Document Approval', + '13' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C1' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'MET', - 'Engineering Document Submittal', - '14' + ct.id, + 'MET', + 'Engineering Document Submittal', + '14' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C1' - AND ct.type_code = 'RFA'; + AND ct.type_code = 'RFA'; + -- LCBP3-C2 INSERT INTO correspondence_sub_types ( - contract_id, - correspondence_type_id, - sub_type_code, - sub_type_name, - sub_type_number - ) + contract_id, + correspondence_type_id, + sub_type_code, + sub_type_name, + sub_type_number + ) SELECT c.id, - ct.id, - 'MAT', - 'Material Approval', - '21' + ct.id, + 'MAT', + 'Material Approval', + '21' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C2' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'SHP', - 'Shop Drawing Submittal', - '22' + ct.id, + 'SHP', + 'Shop Drawing Submittal', + '22' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C2' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'DWG', - 'Document Approval', - '23' + ct.id, + 'DWG', + 'Document Approval', + '23' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C2' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'MET', - 'Engineering Document Submittal', - '24' + ct.id, + 'MET', + 'Engineering Document Submittal', + '24' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C2' - AND ct.type_code = 'RFA'; + AND ct.type_code = 'RFA'; + -- LCBP3-C3 INSERT INTO correspondence_sub_types ( - contract_id, - correspondence_type_id, - sub_type_code, - sub_type_name, - sub_type_number - ) + contract_id, + correspondence_type_id, + sub_type_code, + sub_type_name, + sub_type_number + ) SELECT c.id, - ct.id, - 'MAT', - 'Material Approval', - '31' + ct.id, + 'MAT', + 'Material Approval', + '31' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C3' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'SHP', - 'Shop Drawing Submittal', - '32' + ct.id, + 'SHP', + 'Shop Drawing Submittal', + '32' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C3' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'DWG', - 'Document Approval', - '33' + ct.id, + 'DWG', + 'Document Approval', + '33' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C3' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'MET', - 'Engineering Document Submittal', - '34' + ct.id, + 'MET', + 'Engineering Document Submittal', + '34' FROM contracts c, - correspondence_types ct -WHERE c.contract_code = 'LCBP3-C4' - AND ct.type_code = 'RFA'; --- Note: 6B data has C4 on the right column for MET but C3 on left, checking logic... MD says C3 for first 3 rows, then C4 mixed. I will assume C4 starts at row 12 in the MD table. + correspondence_types ct +WHERE c.contract_code = 'LCBP3-C3' + AND ct.type_code = 'RFA'; + -- LCBP3-C4 INSERT INTO correspondence_sub_types ( - contract_id, - correspondence_type_id, - sub_type_code, - sub_type_name, - sub_type_number - ) + contract_id, + correspondence_type_id, + sub_type_code, + sub_type_name, + sub_type_number + ) SELECT c.id, - ct.id, - 'MAT', - 'Material Approval', - '41' + ct.id, + 'MAT', + 'Material Approval', + '41' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C4' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'SHP', - 'Shop Drawing Submittal', - '42' + ct.id, + 'SHP', + 'Shop Drawing Submittal', + '42' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C4' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'DWG', - 'Document Approval', - '43' + ct.id, + 'DWG', + 'Document Approval', + '43' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C4' - AND ct.type_code = 'RFA' + AND ct.type_code = 'RFA' UNION ALL SELECT c.id, - ct.id, - 'MET', - 'Engineering Document Submittal', - '44' + ct.id, + 'MET', + 'Engineering Document Submittal', + '44' FROM contracts c, - correspondence_types ct + correspondence_types ct WHERE c.contract_code = 'LCBP3-C4' - AND ct.type_code = 'RFA'; + AND ct.type_code = 'RFA'; diff --git a/lcbp3.code-workspace b/lcbp3.code-workspace index 4e80235..c9415df 100644 --- a/lcbp3.code-workspace +++ b/lcbp3.code-workspace @@ -11,10 +11,12 @@ // EDITOR SETTINGS // ======================================== - "editor.fontSize": 15, + "editor.fontSize": 16, + "editor.tabSize": 2, "editor.lineHeight": 1.6, "editor.rulers": [80, 120], "editor.minimap.enabled": true, + "editor.minimap.sectionHeaderFontSize": 14, "editor.renderWhitespace": "boundary", "editor.bracketPairColorization.enabled": true, "editor.guides.bracketPairs": "active", @@ -26,7 +28,6 @@ "editor.formatOnSave": true, "editor.formatOnPaste": true, "editor.suggestSelection": "first", - "editor.tabSize": 2, "editor.detectIndentation": true, // ======================================== @@ -63,7 +64,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[markdown]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.defaultFormatter": "yzhang.markdown-all-in-one", "editor.wordWrap": "on" }, "[yaml]": { @@ -73,27 +74,53 @@ "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "[sql]": { - "editor.defaultFormatter": "renesaarsoo.sql-formatter-vsc" + "editor.defaultFormatter": "mtxr.sqltools", + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.detectIndentation": false, + "editor.wordWrap": "off", + "editor.formatOnSave": true }, - "sql-formatter.dialect": "mysql", - "sql-formatter.indentStyle": "standard", - "sql-formatter.logicalOperatorNewline": "before", - "sql-formatter.expressionWidth": 120, - "sql-formatter.linesBetweenQueries": 2, - "sql-formatter.denseOperators": false, - "sql-formatter.newlineBeforeSemicolon": false, - "sql-formatter.keywordCase": "upper", - "sql-formatter.dataTypeCase": "upper", - "sql-formatter.functionCase": "upper", + "sqltools.format": { + "indent": " ", // 2 spaces + "indentStyle": "space", // ใช้ space แทน tab + "tabSize": 2, + "reservedWordCase": "upper", // คำสงวนเป็นตัวพิมพ์ใหญ่ SELECT, FROM, WHERE + "dataTypeCase": "lower", // varchar, int, datetime + "functionCase": "lower", // count(), sum(), date_format() + // Spacing and Lines + "linesBetweenQueries": 2, // เว้นบรรทัดระหว่าง query + "denseOperators": true, + "spaceAroundOperators": false, + // Comma Style + "commaPosition": "after", // ใส่ comma หลังคอลัมน์ + "newlineBeforeComma": false, + "newlineAfterComma": false, + // Parentheses Style + "newlineBeforeOpenParen": false, + "newlineAfterOpenParen": false, + "newlineBeforeCloseParen": false, + "newlineAfterCloseParen": false, + // Width Control + "expressionWidth": 120, + "wrapLength": 120, + // Other Styles + "compact": true, // ไม่ย่อโค้ดให้แน่นเกินไป + "uppercaseKeywords": true, + "newlineBeforeSemicolon": false + }, + // ป้องกัน extension อื่นมายุ่ง + // ======================================== // CODE ACTION ON SAVE // ======================================== "editor.codeActionsOnSave": { "source.fixAll": "explicit", - // "source.fixAll.eslint": "explicit" - "source.organizeImports": "explicit", - "source.addMissingImports": "explicit" + "source.fixAll.prettier": "explicit", + "source.fixAll.eslint": "explicit" + //"source.organizeImports": "explicit", + //"source.addMissingImports": "explicit" }, // ======================================== @@ -105,12 +132,7 @@ // ======================================== "eslint.enable": true, - "eslint.validate": [ - "javascript", - "javascriptreact", - "typescript", - "typescriptreact" - ], + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], "eslint.alwaysShowStatus": true, "eslint.format.enable": false, "eslint.lintTask.enable": true, @@ -263,24 +285,10 @@ // TODO TREE // ======================================== - "todo-tree.general.tags": [ - "TODO", - "FIXME", - "BUG", - "HACK", - "NOTE", - "XXX", - "[ ]", - "[x]" - ], + "todo-tree.general.tags": ["TODO", "FIXME", "BUG", "HACK", "NOTE", "XXX", "[ ]", "[x]"], "todo-tree.highlights.enabled": true, "todo-tree.tree.showScanModeButton": true, - "todo-tree.filtering.excludeGlobs": [ - "**/node_modules", - "**/dist", - "**/build", - "**/.next" - ], + "todo-tree.filtering.excludeGlobs": ["**/node_modules", "**/dist", "**/build", "**/.next"], "todo-tree.highlights.customHighlight": { "TODO": { "icon": "check", @@ -467,12 +475,17 @@ "workbench.iconTheme": "material-icon-theme", "workbench.activityBar.location": "default", + "workbench.sideBar.location": "left", + "workbench.view.alwaysShowHeaderActions": true, "workbench.tree.indent": 15, "workbench.list.smoothScrolling": true, "workbench.editor.enablePreview": false, "workbench.editor.limit.enabled": true, "workbench.editor.limit.value": 10, "workbench.startupEditor": "welcomePage", + "workbench.view.showQuietly": { + "workbench.panel.output": false + }, // ======================================== // EXPLORER // ======================================== @@ -658,12 +671,8 @@ } ], "database-client.variableIndicator": [":", "$"], - "workbench.colorTheme": "Default Dark Modern", - "workbench.sideBar.location": "left", - "workbench.view.alwaysShowHeaderActions": true, - "workbench.view.showQuietly": { - "workbench.panel.output": false - } + "geminicodeassist.rules": "ใช้ภาษาไทยในการโต้ตอบ\n\n\n\n", + "geminicodeassist.verboseLogging": true }, // ======================================== // LAUNCH CONFIGURATIONS