251204:1700 Prepare to version 1.5.1
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-04 16:50:09 +07:00
parent d33663f7a9
commit 474982af87
34 changed files with 8518 additions and 3107 deletions

View File

@@ -4,7 +4,7 @@
"tabWidth": 2,
"useTabs": false,
"trailingComma": "es5",
"printWidth": 80,
"printWidth": 120,
"arrowParens": "always",
"endOfLine": "lf",
"bracketSpacing": true,

View File

@@ -34,7 +34,6 @@
"wallabyjs.console-ninja",
"pkief.material-icon-theme",
"github.copilot",
"bierner.markdown-mermaid",
"renesaarsoo.sql-formatter-vsc"
"bierner.markdown-mermaid"
]
}

View File

@@ -1,3 +0,0 @@
{
"terminal.integrated.cwd": "\"cwd\": \"D:\\\\nap-dms.lcbp3\\\\frontend\""
}

View File

@@ -0,0 +1,198 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class V1_5_1_Schema_Update1701676800000 implements MigrationInterface {
name = 'V1_5_1_Schema_Update1701676800000';
public async up(queryRunner: QueryRunner): Promise<void> {
// 1. Create Disciplines Table
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`disciplines\` (
\`id\` INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
\`contract_id\` INT NOT NULL COMMENT 'ผูกกับสัญญา',
\`discipline_code\` VARCHAR(10) NOT NULL COMMENT 'รหัสสาขา (เช่น GEN, STR)',
\`code_name_th\` VARCHAR(255) COMMENT 'ชื่อไทย',
\`code_name_en\` VARCHAR(255) COMMENT 'ชื่ออังกฤษ',
\`is_active\` TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน',
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
\`updated_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (\`contract_id\`) REFERENCES \`contracts\` (\`id\`) ON DELETE CASCADE,
UNIQUE KEY \`uk_discipline_contract\` (\`contract_id\`, \`discipline_code\`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บข้อมูลสาขางาน (Disciplines) ตาม Req 6B';
`);
// 2. Create Correspondence Sub Types Table
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`correspondence_sub_types\` (
\`id\` INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
\`contract_id\` INT NOT NULL COMMENT 'ผูกกับสัญญา',
\`correspondence_type_id\` INT NOT NULL COMMENT 'ผูกกับประเภทเอกสารหลัก (เช่น RFA)',
\`sub_type_code\` VARCHAR(20) NOT NULL COMMENT 'รหัสย่อย (เช่น MAT, SHP)',
\`sub_type_name\` VARCHAR(255) COMMENT 'ชื่อประเภทหนังสือย่อย',
\`sub_type_number\` VARCHAR(10) COMMENT 'เลขรหัสสำหรับ Running Number (เช่น 11, 22)',
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (\`contract_id\`) REFERENCES \`contracts\` (\`id\`) ON DELETE CASCADE,
FOREIGN KEY (\`correspondence_type_id\`) REFERENCES \`correspondence_types\` (\`id\`) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บประเภทหนังสือย่อย (Sub Types) ตาม Req 6B';
`);
// 3. Add discipline_id to correspondences
const hasDisciplineCol = await queryRunner.hasColumn(
'correspondences',
'discipline_id'
);
if (!hasDisciplineCol) {
await queryRunner.query(`
ALTER TABLE \`correspondences\`
ADD COLUMN \`discipline_id\` INT NULL COMMENT 'สาขางาน (ถ้ามี)' AFTER \`correspondence_type_id\`,
ADD CONSTRAINT \`fk_corr_discipline\` FOREIGN KEY (\`discipline_id\`) REFERENCES \`disciplines\` (\`id\`) ON DELETE SET NULL;
`);
}
// 4. Add discipline_id to rfas
const hasRfaDisciplineCol = await queryRunner.hasColumn(
'rfas',
'discipline_id'
);
if (!hasRfaDisciplineCol) {
await queryRunner.query(`
ALTER TABLE \`rfas\`
ADD COLUMN \`discipline_id\` INT NULL COMMENT 'สาขางาน (ถ้ามี)' AFTER \`rfa_type_id\`,
ADD CONSTRAINT \`fk_rfa_discipline\` FOREIGN KEY (\`discipline_id\`) REFERENCES \`disciplines\` (\`id\`) ON DELETE SET NULL;
`);
}
// 5. Create Document Numbering Audit & Errors
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`document_number_audit\` (
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
\`project_id\` INT NOT NULL,
\`correspondence_type_id\` INT NOT NULL,
\`running_number\` INT NOT NULL,
\`full_document_number\` VARCHAR(100) NOT NULL,
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
\`created_by\` INT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`document_number_errors\` (
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
\`error_code\` VARCHAR(50),
\`error_message\` TEXT,
\`context_data\` JSON,
\`occurred_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
`);
// 6. Create RFA Items (Linking RFA to Shop Drawings)
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`rfa_items\` (
\`rfarev_correspondence_id\` INT COMMENT 'ID ของ RFA Revision',
\`shop_drawing_revision_id\` INT COMMENT 'ID ของ Shop Drawing Revision',
PRIMARY KEY (\`rfarev_correspondence_id\`, \`shop_drawing_revision_id\`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
`);
// 7. Create Transmittal Tables
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`transmittals\` (
\`id\` INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
\`correspondence_id\` INT UNIQUE COMMENT 'ID ของเอกสาร',
\`transmittal_no\` VARCHAR(100) NOT NULL COMMENT 'เลขที่ใบนำส่ง',
\`subject\` VARCHAR(500) NOT NULL COMMENT 'เรื่อง',
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (\`correspondence_id\`) REFERENCES \`correspondences\` (\`id\`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`transmittal_items\` (
\`transmittal_id\` INT NOT NULL,
\`item_type\` VARCHAR(50) NOT NULL COMMENT 'ประเภทรายการ (DRAWING, RFA, etc.)',
\`item_id\` INT NOT NULL COMMENT 'ID ของรายการ',
\`description\` TEXT,
PRIMARY KEY (\`transmittal_id\`, \`item_type\`, \`item_id\`),
FOREIGN KEY (\`transmittal_id\`) REFERENCES \`transmittals\` (\`id\`) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
`);
// 8. Create Circulation Tables
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`circulation_status_codes\` (
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
\`code\` VARCHAR(20) NOT NULL UNIQUE,
\`description\` VARCHAR(50) NOT NULL,
\`sort_order\` INT DEFAULT 0,
\`is_active\` TINYINT(1) DEFAULT 1
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`circulations\` (
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
\`correspondence_id\` INT UNIQUE,
\`organization_id\` INT NOT NULL,
\`circulation_no\` VARCHAR(100) NOT NULL,
\`circulation_subject\` VARCHAR(500) NOT NULL,
\`circulation_status_code\` VARCHAR(20) NOT NULL,
\`created_by_user_id\` INT NOT NULL,
\`submitted_at\` TIMESTAMP NULL,
\`closed_at\` TIMESTAMP NULL,
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
\`updated_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (\`correspondence_id\`) REFERENCES \`correspondences\` (\`id\`),
FOREIGN KEY (\`organization_id\`) REFERENCES \`organizations\` (\`id\`),
FOREIGN KEY (\`circulation_status_code\`) REFERENCES \`circulation_status_codes\` (\`code\`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS \`circulation_attachments\` (
\`circulation_id\` INT NOT NULL,
\`attachment_id\` INT NOT NULL,
PRIMARY KEY (\`circulation_id\`, \`attachment_id\`),
FOREIGN KEY (\`circulation_id\`) REFERENCES \`circulations\` (\`id\`) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop tables in reverse order
await queryRunner.query(`DROP TABLE IF EXISTS circulation_attachments`);
await queryRunner.query(`DROP TABLE IF EXISTS circulations`);
await queryRunner.query(`DROP TABLE IF EXISTS circulation_status_codes`);
await queryRunner.query(`DROP TABLE IF EXISTS transmittal_items`);
await queryRunner.query(`DROP TABLE IF EXISTS transmittals`);
await queryRunner.query(`DROP TABLE IF EXISTS rfa_items`);
await queryRunner.query(`DROP TABLE IF EXISTS document_number_errors`);
await queryRunner.query(`DROP TABLE IF EXISTS document_number_audit`);
// Remove columns
const hasRfaDisciplineCol = await queryRunner.hasColumn(
'rfas',
'discipline_id'
);
if (hasRfaDisciplineCol) {
await queryRunner.query(
`ALTER TABLE rfas DROP FOREIGN KEY fk_rfa_discipline`
);
await queryRunner.query(`ALTER TABLE rfas DROP COLUMN discipline_id`);
}
const hasDisciplineCol = await queryRunner.hasColumn(
'correspondences',
'discipline_id'
);
if (hasDisciplineCol) {
await queryRunner.query(
`ALTER TABLE correspondences DROP FOREIGN KEY fk_corr_discipline`
);
await queryRunner.query(
`ALTER TABLE correspondences DROP COLUMN discipline_id`
);
}
await queryRunner.query(`DROP TABLE IF EXISTS correspondence_sub_types`);
await queryRunner.query(`DROP TABLE IF EXISTS disciplines`);
}
}

View File

@@ -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],

View File

@@ -5,14 +5,15 @@ import {
ForbiddenException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, Not } from 'typeorm'; // เพิ่ม Not
import { Repository, DataSource, Not } from 'typeorm';
import { Circulation } from './entities/circulation.entity';
import { CirculationRouting } from './entities/circulation-routing.entity';
import { User } from '../user/entities/user.entity';
import { CreateCirculationDto } from './dto/create-circulation.dto';
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto'; // Import ใหม่
import { SearchCirculationDto } from './dto/search-circulation.dto'; // Import ใหม่
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto';
import { SearchCirculationDto } from './dto/search-circulation.dto';
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
@Injectable()
export class CirculationService {
@@ -21,7 +22,8 @@ export class CirculationService {
private circulationRepo: Repository<Circulation>,
@InjectRepository(CirculationRouting)
private routingRepo: Repository<CirculationRouting>,
private dataSource: DataSource,
private numberingService: DocumentNumberingService,
private dataSource: DataSource
) {}
async create(createDto: CreateCirculationDto, user: User) {
@@ -34,8 +36,17 @@ export class CirculationService {
await queryRunner.startTransaction();
try {
// Generate No. (Mock Logic) -> ควรใช้ NumberingService จริงในอนาคต
const circulationNo = `CIR-${Date.now()}`;
// Generate No. using DocumentNumberingService (Type 900 - Circulation)
const circulationNo = await this.numberingService.generateNextNumber({
projectId: createDto.projectId || 0, // Use projectId from DTO or 0
originatorId: user.primaryOrganizationId,
typeId: 900, // Fixed Type ID for Circulation
year: new Date().getFullYear(),
customTokens: {
TYPE_CODE: 'CIR',
ORG_CODE: 'ORG',
},
});
const circulation = queryRunner.manager.create(Circulation, {
organizationId: user.primaryOrganizationId,
@@ -55,7 +66,7 @@ export class CirculationService {
organizationId: user.primaryOrganizationId,
assignedTo: userId,
status: 'PENDING',
}),
})
);
await queryRunner.manager.save(routings);
}
@@ -83,13 +94,6 @@ export class CirculationService {
query.andWhere('c.statusCode = :status', { status });
}
if (search) {
query.andWhere(
'(c.circulationNo LIKE :search OR c.subject LIKE :search)',
{ search: `%${search}%` },
);
}
query
.orderBy('c.createdAt', 'DESC')
.skip((page - 1) * limit)
@@ -113,7 +117,7 @@ export class CirculationService {
async updateRoutingStatus(
routingId: number,
dto: UpdateCirculationRoutingDto,
user: User,
user: User
) {
const routing = await this.routingRepo.findOne({
where: { id: routingId },

View File

@@ -12,6 +12,10 @@ export class CreateCirculationDto {
@IsNotEmpty()
correspondenceId!: number; // เอกสารต้นเรื่องที่จะเวียน
@IsInt()
@IsOptional()
projectId?: number; // Project ID for Numbering
@IsString()
@IsNotEmpty()
subject!: string; // หัวข้อเรื่อง (Subject)

View File

@@ -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}`
);
}
}
@@ -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);
}

View File

@@ -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[];
}

View File

@@ -1,8 +1,6 @@
import {
Injectable,
NotFoundException,
BadRequestException,
InternalServerErrorException,
ConflictException,
Logger,
} from '@nestjs/common';
@@ -38,7 +36,7 @@ export class ShopDrawingService {
@InjectRepository(Attachment)
private attachmentRepo: Repository<Attachment>,
private fileStorageService: FileStorageService,
private dataSource: DataSource,
private dataSource: DataSource
) {}
/**
@@ -51,7 +49,7 @@ export class ShopDrawingService {
});
if (exists) {
throw new ConflictException(
`Drawing number "${createDto.drawingNumber}" already exists.`,
`Drawing number "${createDto.drawingNumber}" already exists.`
);
}
@@ -103,7 +101,7 @@ export class ShopDrawingService {
// 5. Commit Files
if (createDto.attachmentIds?.length) {
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
createDto.attachmentIds.map(String)
);
}
@@ -117,7 +115,7 @@ export class ShopDrawingService {
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Failed to create shop drawing: ${(err as Error).message}`,
`Failed to create shop drawing: ${(err as Error).message}`
);
throw err;
} finally {
@@ -130,7 +128,7 @@ export class ShopDrawingService {
*/
async createRevision(
shopDrawingId: number,
createDto: CreateShopDrawingRevisionDto,
createDto: CreateShopDrawingRevisionDto
) {
const shopDrawing = await this.shopDrawingRepo.findOneBy({
id: shopDrawingId,
@@ -144,7 +142,7 @@ export class ShopDrawingService {
});
if (exists) {
throw new ConflictException(
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`,
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`
);
}
@@ -188,7 +186,7 @@ export class ShopDrawingService {
if (createDto.attachmentIds?.length) {
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
createDto.attachmentIds.map(String)
);
}
@@ -237,7 +235,7 @@ export class ShopDrawingService {
qb.where('sd.drawingNumber LIKE :search', {
search: `%${search}%`,
}).orWhere('sd.title LIKE :search', { search: `%${search}%` });
}),
})
);
}

View File

@@ -0,0 +1,70 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
ParseIntPipe,
Query,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiBearerAuth,
ApiQuery,
} from '@nestjs/swagger';
import { ContractService } from './contract.service.js';
import { CreateContractDto } from './dto/create-contract.dto.js';
import { UpdateContractDto } from './dto/update-contract.dto.js';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
@ApiTags('Contracts')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('contracts')
export class ContractController {
constructor(private readonly contractService: ContractService) {}
@Post()
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Create Contract' })
create(@Body() dto: CreateContractDto) {
return this.contractService.create(dto);
}
@Get()
@ApiOperation({
summary: 'Get All Contracts (Optional: filter by projectId)',
})
@ApiQuery({ name: 'projectId', required: false, type: Number })
findAll(@Query('projectId') projectId?: number) {
return this.contractService.findAll(projectId);
}
@Get(':id')
@ApiOperation({ summary: 'Get Contract by ID' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.contractService.findOne(id);
}
@Patch(':id')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Update Contract' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateContractDto
) {
return this.contractService.update(id, dto);
}
@Delete(':id')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Delete Contract' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.contractService.remove(id);
}
}

View File

@@ -0,0 +1,65 @@
import {
Injectable,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Contract } from './entities/contract.entity.js';
import { CreateContractDto } from './dto/create-contract.dto.js';
import { UpdateContractDto } from './dto/update-contract.dto.js';
@Injectable()
export class ContractService {
constructor(
@InjectRepository(Contract)
private readonly contractRepo: Repository<Contract>
) {}
async create(dto: CreateContractDto) {
const existing = await this.contractRepo.findOne({
where: { contractCode: dto.contractCode },
});
if (existing) {
throw new ConflictException(
`Contract Code "${dto.contractCode}" already exists`
);
}
const contract = this.contractRepo.create(dto);
return this.contractRepo.save(contract);
}
async findAll(projectId?: number) {
const query = this.contractRepo
.createQueryBuilder('c')
.leftJoinAndSelect('c.project', 'p')
.orderBy('c.contractCode', 'ASC');
if (projectId) {
query.where('c.projectId = :projectId', { projectId });
}
return query.getMany();
}
async findOne(id: number) {
const contract = await this.contractRepo.findOne({
where: { id },
relations: ['project'],
});
if (!contract) throw new NotFoundException(`Contract ID ${id} not found`);
return contract;
}
async update(id: number, dto: UpdateContractDto) {
const contract = await this.findOne(id);
Object.assign(contract, dto);
return this.contractRepo.save(contract);
}
async remove(id: number) {
const contract = await this.findOne(id);
// Schema doesn't have deleted_at for Contract either.
return this.contractRepo.remove(contract);
}
}

View File

@@ -0,0 +1,49 @@
import {
IsString,
IsNotEmpty,
IsBoolean,
IsOptional,
Length,
IsInt,
IsDateString,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateContractDto {
@ApiProperty({ example: 1 })
@IsInt()
@IsNotEmpty()
projectId!: number;
@ApiProperty({ example: 'C-001' })
@IsString()
@IsNotEmpty()
@Length(1, 50)
contractCode!: string;
@ApiProperty({ example: 'Main Construction Contract' })
@IsString()
@IsNotEmpty()
@Length(1, 255)
contractName!: string;
@ApiProperty({ example: 'Description of the contract', required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ example: '2024-01-01', required: false })
@IsOptional()
@IsDateString()
startDate?: string; // Receive as string, TypeORM handles date
@ApiProperty({ example: '2025-12-31', required: false })
@IsOptional()
@IsDateString()
endDate?: string;
@ApiProperty({ example: true, required: false })
@IsOptional()
@IsBoolean()
isActive?: boolean;
}

View File

@@ -0,0 +1,27 @@
import {
IsString,
IsNotEmpty,
IsBoolean,
IsOptional,
Length,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateOrganizationDto {
@ApiProperty({ example: 'ITD' })
@IsString()
@IsNotEmpty()
@Length(1, 20)
organizationCode!: string;
@ApiProperty({ example: 'Italian-Thai Development' })
@IsString()
@IsNotEmpty()
@Length(1, 255)
organizationName!: string;
@ApiProperty({ example: true, required: false })
@IsOptional()
@IsBoolean()
isActive?: boolean;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateContractDto } from './create-contract.dto.js';
export class UpdateContractDto extends PartialType(CreateContractDto) {}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateOrganizationDto } from './create-organization.dto.js';
export class UpdateOrganizationDto extends PartialType(CreateOrganizationDto) {}

View File

@@ -0,0 +1,61 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { OrganizationService } from './organization.service.js';
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
@ApiTags('Organizations')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('organizations')
export class OrganizationController {
constructor(private readonly orgService: OrganizationService) {}
@Post()
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Create Organization' })
create(@Body() dto: CreateOrganizationDto) {
return this.orgService.create(dto);
}
@Get()
@ApiOperation({ summary: 'Get All Organizations' })
findAll() {
return this.orgService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get Organization by ID' })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.orgService.findOne(id);
}
@Patch(':id')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Update Organization' })
update(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateOrganizationDto
) {
return this.orgService.update(id, dto);
}
@Delete(':id')
@RequirePermission('master_data.manage')
@ApiOperation({ summary: 'Delete Organization' })
remove(@Param('id', ParseIntPipe) id: number) {
return this.orgService.remove(id);
}
}

View File

@@ -0,0 +1,57 @@
import {
Injectable,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Organization } from './entities/organization.entity.js';
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
@Injectable()
export class OrganizationService {
constructor(
@InjectRepository(Organization)
private readonly orgRepo: Repository<Organization>
) {}
async create(dto: CreateOrganizationDto) {
const existing = await this.orgRepo.findOne({
where: { organizationCode: dto.organizationCode },
});
if (existing) {
throw new ConflictException(
`Organization Code "${dto.organizationCode}" already exists`
);
}
const org = this.orgRepo.create(dto);
return this.orgRepo.save(org);
}
async findAll() {
return this.orgRepo.find({
order: { organizationCode: 'ASC' },
});
}
async findOne(id: number) {
const org = await this.orgRepo.findOne({ where: { id } });
if (!org) throw new NotFoundException(`Organization ID ${id} not found`);
return org;
}
async update(id: number, dto: UpdateOrganizationDto) {
const org = await this.findOne(id);
Object.assign(org, dto);
return this.orgRepo.save(org);
}
async remove(id: number) {
const org = await this.findOne(id);
// Hard delete or Soft delete? Schema doesn't have deleted_at for Organization, but let's check.
// Schema says: created_at, updated_at. No deleted_at.
// So hard delete.
return this.orgRepo.remove(org);
}
}

View File

@@ -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 {}

View File

@@ -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,18 +171,38 @@ 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({
this.searchService
.indexDocument({
id: savedCorr.id,
type: 'rfa',
docNumber: docNumber,
@@ -194,7 +211,8 @@ export class RfaService {
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;

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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' })

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -1,98 +1,143 @@
// File: src/modules/transmittal/transmittal.service.ts
import {
Injectable,
Logger,
NotFoundException,
InternalServerErrorException,
BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In } from 'typeorm';
import { Transmittal } from './entities/transmittal.entity.js';
import { TransmittalItem } from './entities/transmittal-item.entity.js';
import { Correspondence } from '../correspondence/entities/correspondence.entity.js';
import { CreateTransmittalDto } from './dto/create-transmittal.dto.js';
import { User } from '../user/entities/user.entity.js';
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
import { SearchService } from '../search/search.service.js';
import { Repository, DataSource } from 'typeorm';
import { Transmittal } from './entities/transmittal.entity';
import { TransmittalItem } from './entities/transmittal-item.entity';
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
import { User } from '../user/entities/user.entity';
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
import { Correspondence } from '../correspondence/entities/correspondence.entity';
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
@Injectable()
export class TransmittalService {
private readonly logger = new Logger(TransmittalService.name);
constructor(
@InjectRepository(Transmittal)
private transmittalRepo: Repository<Transmittal>,
@InjectRepository(TransmittalItem)
private transmittalItemRepo: Repository<TransmittalItem>,
@InjectRepository(Correspondence)
private correspondenceRepo: Repository<Correspondence>,
private itemRepo: Repository<TransmittalItem>,
@InjectRepository(CorrespondenceType)
private typeRepo: Repository<CorrespondenceType>,
@InjectRepository(CorrespondenceStatus)
private statusRepo: Repository<CorrespondenceStatus>,
private numberingService: DocumentNumberingService,
private dataSource: DataSource,
private searchService: SearchService,
private dataSource: DataSource
) {}
async create(createDto: CreateTransmittalDto, user: User) {
if (!user.primaryOrganizationId) {
throw new BadRequestException(
'User must belong to an organization to create documents',
);
}
const userOrgId = user.primaryOrganizationId;
// 1. Get Transmittal Type (Assuming Code '901' or 'TRN')
const type = await this.typeRepo.findOne({
where: { typeCode: 'TRN' }, // Adjust code as per Master Data
});
if (!type) throw new NotFoundException('Transmittal Type (TRN) not found');
const statusDraft = await this.statusRepo.findOne({
where: { statusCode: 'DRAFT' },
});
if (!statusDraft)
throw new InternalServerErrorException('Status DRAFT not found');
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const transmittalTypeId = 3; // TODO: ดึง ID จริงจาก DB หรือ Config
const orgCode = 'ORG'; // TODO: Fetch real ORG Code
if (!user.primaryOrganizationId) {
throw new BadRequestException(
'User must belong to an organization to create a transmittal'
);
}
// [FIXED] เรียกใช้แบบ Object Context
try {
// 2. Generate Number
const docNumber = await this.numberingService.generateNextNumber({
projectId: createDto.projectId,
originatorId: userOrgId,
typeId: transmittalTypeId,
originatorId: user.primaryOrganizationId,
typeId: type.id,
year: new Date().getFullYear(),
customTokens: {
TYPE_CODE: 'TR',
ORG_CODE: orgCode,
TYPE_CODE: type.typeCode,
ORG_CODE: 'ORG', // TODO: Fetch real ORG Code
},
});
// 3. Create Correspondence (Parent)
const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber,
correspondenceTypeId: transmittalTypeId,
correspondenceTypeId: type.id,
projectId: createDto.projectId,
originatorId: userOrgId,
originatorId: user.primaryOrganizationId,
isInternal: false,
createdBy: user.user_id,
});
const savedCorr = await queryRunner.manager.save(correspondence);
// 4. Create Revision (Draft)
const revision = queryRunner.manager.create(CorrespondenceRevision, {
correspondenceId: savedCorr.id,
revisionNumber: 0,
revisionLabel: '0',
isCurrent: true,
statusId: statusDraft.id,
title: createDto.subject,
createdBy: user.user_id,
});
await queryRunner.manager.save(revision);
// 5. Create Transmittal
const transmittal = queryRunner.manager.create(Transmittal, {
correspondenceId: savedCorr.id,
purpose: createDto.purpose,
remarks: createDto.remarks,
transmittalNo: docNumber,
subject: createDto.subject,
});
await queryRunner.manager.save(transmittal);
const savedTransmittal = await queryRunner.manager.save(transmittal);
if (createDto.itemIds && createDto.itemIds.length > 0) {
const items = createDto.itemIds.map((itemId) =>
// 6. Create Items
if (createDto.items && createDto.items.length > 0) {
const items = createDto.items.map((item) =>
queryRunner.manager.create(TransmittalItem, {
transmittalId: savedCorr.id,
itemCorrespondenceId: itemId,
quantity: 1,
}),
transmittalId: savedTransmittal.id,
itemType: item.itemType,
itemId: item.itemId,
description: item.description,
})
);
await queryRunner.manager.save(items);
}
await queryRunner.commitTransaction();
return { ...savedCorr, transmittal };
return {
...savedTransmittal,
correspondence: savedCorr,
};
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Failed to create transmittal: ${(err as Error).message}`
);
throw err;
} finally {
await queryRunner.release();
}
}
async findOne(id: number) {
const transmittal = await this.transmittalRepo.findOne({
where: { id },
relations: ['correspondence', 'items'],
});
if (!transmittal)
throw new NotFoundException(`Transmittal ID ${id} not found`);
return transmittal;
}
}

View File

@@ -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. NonFunctional 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** 20251204 Consolidated requirement specifications into single document.

1448
docs/1_FullStackJS_V1_5_1.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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**

View File

@@ -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**

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,116 @@
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 ),
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 );
(
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 (
@@ -50,6 +137,7 @@ VALUES (
'LCBP3-EN',
'โครงการพัฒนาท่าเรือแหลมฉบัง ระยะที่ 3 (ส่วนที่ 4) งานก่อสร้าง'
);
-- Seed contract
-- ใช้ Subquery เพื่อดึง project_id มาเชื่อมโยง ทำให้ไม่ต้องมานั่งจัดการ ID ด้วยตัวเอง
INSERT INTO contracts (
@@ -128,6 +216,7 @@ VALUES (
),
TRUE
);
-- Seed user
-- Initial SUPER_ADMIN user
INSERT INTO users (
@@ -180,6 +269,7 @@ VALUES (
NULL,
10
);
-- ==========================================================
-- Seed Roles (บทบาทพื้นฐาน 5 บทบาท ตาม Req 4.3)
-- ==========================================================
@@ -190,7 +280,8 @@ VALUES (
'Superadmin',
'Global',
'ผู้ดูแลระบบสูงสุด: สามารถทำทุกอย่างในระบบ, จัดการองค์กร, และจัดการข้อมูลหลักระดับ Global'
).-- 2. Org Admin (Organization)
),
-- 2. Org Admin (Organization)
(
2,
'Org Admin',
@@ -232,6 +323,7 @@ VALUES (
'Contract',
'ผู้ดูแลสัญญา: จัดการสมาชิกในสัญญา, สร้าง / จัดการข้อมูลหลักเฉพาะสัญญา, และอนุมัติเอกสารในสัญญา'
);
-- =====================================================
-- 2. Seed Permissions (สิทธิ์การใช้งานทั้งหมด)
-- สิทธิ์ระดับระบบและการจัดการหลัก (System & Master Data)
@@ -278,11 +370,7 @@ VALUES (
'project.edit',
'แก้ไขข้อมูลโครงการ'
),
(
8,
'project.delete',
'ลบโครงการ'
),
(8, 'project.delete', 'ลบโครงการ'),
(
9,
'project.view',
@@ -299,11 +387,7 @@ VALUES (
'role.edit',
'แก้ไขบทบาท (Role)'
),
(
12,
'role.delete',
'ลบบทบาท (Role)'
),
(12, 'role.delete', 'ลบบทบาท (Role)'),
(
13,
'permission.assign',
@@ -356,6 +440,7 @@ VALUES (
'user.assign_organization',
'มอบผู้ใช้งานให้กับองค์กร'
);
-- =====================================================
-- == 2. สิทธิ์การจัดการโครงการและสัญญา (Project & Contract) ==
-- =====================================================
@@ -394,6 +479,7 @@ VALUES (
'contract.view',
'ดูข้อมูลสัญญา'
);
-- =====================================================
-- == 3. สิทธิ์การจัดการเอกสาร (Document Management) ==
-- =====================================================
@@ -413,11 +499,7 @@ VALUES (
'document.submit',
'ส่งเอกสาร (Submitted)'
),
(
31,
'document.view',
'ดูเอกสาร'
),
(31, 'document.view', 'ดูเอกสาร'),
(
32,
'document.edit',
@@ -428,11 +510,7 @@ VALUES (
'document.admin_edit',
'แก้ไข / ถอน / ยกเลิกเอกสารที่ส่งแล้ว (Admin Power) '
),
(
34,
'document.delete',
'ลบเอกสาร'
),
(34, 'document.delete', 'ลบเอกสาร'),
(
35,
'document.attach',
@@ -488,6 +566,7 @@ VALUES (
'circulation.close',
'ปิดใบเวียน'
);
-- =====================================================
-- == 4. สิทธิ์การจัดการ Workflow ==
-- =====================================================
@@ -511,6 +590,7 @@ VALUES (
'workflow.revert',
'ย้อนกลับไปยังขั้นตอนก่อนหน้า (Document Control Power)'
);
-- =====================================================
-- == 5. สิทธิ์ด้านการค้นหาและรายงาน (Search & Reporting) ==
-- =====================================================
@@ -529,6 +609,7 @@ VALUES (
'report.generate',
'สร้างรายงานสรุป (รายวัน / สัปดาห์ / เดือน / ปี)'
);
-- ==========================================================
-- Seed Role-Permissions Mapping (จับคู่สิทธิ์เริ่มต้น)
-- ==========================================================
@@ -545,6 +626,7 @@ INSERT INTO role_permissions (role_id, permission_id)
SELECT 1,
permission_id
FROM permissions;
-- =====================================================
-- == 2. Org Admin (role_id = 2) ==
-- =====================================================
@@ -579,6 +661,7 @@ VALUES -- จัดการผู้ใช้ในองค์กร
(2, 48),
-- search.advanced
(2, 49);
-- report.generate
-- =====================================================
-- == 3. Document Control (role_id = 3) ==
@@ -630,6 +713,7 @@ VALUES -- สิทธิ์จัดการเอกสารทั้งห
(3, 48),
-- search.advanced
(3, 49);
-- report.generate
-- =====================================================
-- == 4. Editor (role_id = 4) ==
@@ -661,6 +745,7 @@ VALUES -- สิทธิ์แก้ไขเอกสาร (แต่ไม
(4, 38),
-- rfa.manage_shop_drawings
(4, 48);
-- search.advanced
-- =====================================================
-- == 5. Viewer (role_id = 5) ==
@@ -670,6 +755,7 @@ VALUES -- สิทธิ์ดูเท่านั้น
(5, 31),
-- document.view
(5, 48);
-- search.advanced
-- =====================================================
-- == 6. Project Manager (role_id = 6) ==
@@ -718,6 +804,7 @@ VALUES -- สิทธิ์จัดการโครงการ
(6, 48),
-- search.advanced
(6, 49);
-- report.generate
-- =====================================================
-- == 7. Contract Admin (role_id = 7) ==
@@ -756,6 +843,7 @@ VALUES -- สิทธิ์จัดการสัญญา
(7, 41),
-- circulation.create
(7, 48);
-- Seed data for the 'user_assignments' table
INSERT INTO `user_assignments` (
`id`,
@@ -776,6 +864,7 @@ VALUES (
NULL
),
(2, 2, 2, 1, NULL, NULL, NULL);
-- =====================================================
-- == 4. การเชื่อมโยงโครงการกับองค์กร (project_organizations) ==
-- =====================================================
@@ -800,6 +889,7 @@ WHERE organization_code IN (
'EN',
'CAR'
);
-- โครงการย่อย (LCBP3C1) จะมีเฉพาะองค์กรที่เกี่ยวข้อง
INSERT INTO project_organizations (project_id, organization_id)
SELECT (
@@ -816,6 +906,7 @@ WHERE organization_code IN (
'คคง.',
'ผรม.1 '
);
-- ทำเช่นเดียวกันสำหรับโครงการอื่นๆ (ตัวอย่าง)
INSERT INTO project_organizations (project_id, organization_id)
SELECT (
@@ -832,6 +923,7 @@ WHERE organization_code IN (
'คคง.',
'ผรม.2'
);
-- =====================================================
-- == 5. การเชื่อมโยงสัญญากับองค์กร (contract_organizations) ==
-- =====================================================
@@ -867,6 +959,7 @@ VALUES (
),
'Designer'
);
-- สัญญาที่ปรึกษาควบคุมงาน (PSLCBP3)
INSERT INTO contract_organizations (
contract_id,
@@ -899,6 +992,7 @@ VALUES (
),
'Consultant'
);
-- สัญญางานก่อสร้าง ส่วนที่ 1 (LCBP3-C1)
INSERT INTO contract_organizations (
contract_id,
@@ -931,6 +1025,7 @@ VALUES (
),
'Contractor'
);
-- สัญญางานก่อสร้าง ส่วนที่ 2 (LCBP3-C2)
INSERT INTO contract_organizations (
contract_id,
@@ -963,6 +1058,7 @@ VALUES (
),
'Contractor'
);
-- สัญญาตรวจสอบสิ่งแวดล้อม (LCBP3-EN)
INSERT INTO contract_organizations (
contract_id,
@@ -995,6 +1091,7 @@ VALUES (
),
'Consultant'
);
-- Seed correspondence_status
INSERT INTO correspondence_status (
status_code,
@@ -1045,12 +1142,7 @@ VALUES ('DRAFT', 'Draft', 10, 1),
32,
1
),
(
'REPCSC',
'Reply by CSC',
33,
1
),
('REPCSC', 'Reply by CSC', 33, 1),
(
'REPCON',
'Reply by Contractor',
@@ -1099,12 +1191,7 @@ VALUES ('DRAFT', 'Draft', 10, 1),
52,
1
),
(
'CLBCSC',
'Closed by CSC',
53,
1
),
('CLBCSC', 'Closed by CSC', 53, 1),
(
'CLBCON',
'Closed by Contractor',
@@ -1135,6 +1222,7 @@ VALUES ('DRAFT', 'Draft', 10, 1),
94,
1
);
-- Seed correspondence_types
INSERT INTO correspondence_types (
type_code,
@@ -1177,6 +1265,7 @@ VALUES (
),
('NOTICE', 'Notice', 9, 1),
('OTHER', 'Other', 10, 1);
-- Seed rfa_types
INSERT INTO rfa_types (
contract_id,
@@ -1666,6 +1755,7 @@ SELECT id,
'รายงานการฝึกปฏิบัติ'
FROM contracts
WHERE contract_code = 'LCBP3-C2';
-- Seed rfa_status_codes
INSERT INTO rfa_status_codes (
status_code,
@@ -1704,12 +1794,8 @@ VALUES ('DFT', 'Draft', 'ฉบับร่าง', 1),
'ไม่ใช้งาน',
80
),
(
'CC',
'Canceled',
'ยกเลิก',
99
);
('CC', 'Canceled', 'ยกเลิก', 99);
INSERT INTO rfa_approve_codes (
approve_code,
approve_name,
@@ -1722,18 +1808,8 @@ VALUES (
10,
1
),
(
'1C',
'Approved by CSC',
11,
1
),
(
'1N',
'Approved As Note',
12,
1
),
('1C', 'Approved by CSC', 11, 1),
('1N', 'Approved As Note', 12, 1),
(
'1R',
'Approved with Remarks',
@@ -1754,12 +1830,8 @@ VALUES (
1
),
('4X', 'Reject', 40, 1),
(
'5N',
'No Further Action',
50,
1
);
('5N', 'No Further Action', 50, 1);
-- Seed circulation_status_codes
INSERT INTO circulation_status_codes (code, description, sort_order)
VALUES ('OPEN', 'Open', 1),
@@ -1770,6 +1842,7 @@ VALUES ('OPEN', 'Open', 1),
'Cancelled / Withdrawn',
9
);
-- ตาราง "แม่" ของ RFA (มีความสัมพันธ์ 1:N กับ rfa_revisions)
-- ==========================================================
-- SEED DATA 6B.md (Disciplines, RFA Types, Sub Types)
@@ -2033,6 +2106,7 @@ SELECT id,
'Other'
FROM contracts
WHERE contract_code = 'LCBP3-C1';
-- LCBP3-C2
INSERT INTO disciplines (
contract_id,
@@ -2277,14 +2351,17 @@ SELECT id,
'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,
sub_type_number
)
SELECT c.id,
ct.id,
@@ -2325,6 +2402,7 @@ FROM contracts c,
correspondence_types ct
WHERE c.contract_code = 'LCBP3-C1'
AND ct.type_code = 'RFA';
-- LCBP3-C2
INSERT INTO correspondence_sub_types (
contract_id,
@@ -2372,6 +2450,7 @@ FROM contracts c,
correspondence_types ct
WHERE c.contract_code = 'LCBP3-C2'
AND ct.type_code = 'RFA';
-- LCBP3-C3
INSERT INTO correspondence_sub_types (
contract_id,
@@ -2417,9 +2496,9 @@ SELECT c.id,
'34'
FROM contracts c,
correspondence_types ct
WHERE c.contract_code = 'LCBP3-C4'
WHERE c.contract_code = 'LCBP3-C3'
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.
-- LCBP3-C4
INSERT INTO correspondence_sub_types (
contract_id,

View File

@@ -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