251204:1700 Prepare to version 1.5.1
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80,
|
||||
"printWidth": 120,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"bracketSpacing": true,
|
||||
|
||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -34,7 +34,6 @@
|
||||
"wallabyjs.console-ninja",
|
||||
"pkief.material-icon-theme",
|
||||
"github.copilot",
|
||||
"bierner.markdown-mermaid",
|
||||
"renesaarsoo.sql-formatter-vsc"
|
||||
"bierner.markdown-mermaid"
|
||||
]
|
||||
}
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"terminal.integrated.cwd": "\"cwd\": \"D:\\\\nap-dms.lcbp3\\\\frontend\""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class V1_5_1_Schema_Update1701676800000 implements MigrationInterface {
|
||||
name = 'V1_5_1_Schema_Update1701676800000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 1. Create Disciplines Table
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`disciplines\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||
\`contract_id\` INT NOT NULL COMMENT 'ผูกกับสัญญา',
|
||||
\`discipline_code\` VARCHAR(10) NOT NULL COMMENT 'รหัสสาขา (เช่น GEN, STR)',
|
||||
\`code_name_th\` VARCHAR(255) COMMENT 'ชื่อไทย',
|
||||
\`code_name_en\` VARCHAR(255) COMMENT 'ชื่ออังกฤษ',
|
||||
\`is_active\` TINYINT(1) DEFAULT 1 COMMENT 'สถานะการใช้งาน',
|
||||
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (\`contract_id\`) REFERENCES \`contracts\` (\`id\`) ON DELETE CASCADE,
|
||||
UNIQUE KEY \`uk_discipline_contract\` (\`contract_id\`, \`discipline_code\`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บข้อมูลสาขางาน (Disciplines) ตาม Req 6B';
|
||||
`);
|
||||
|
||||
// 2. Create Correspondence Sub Types Table
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`correspondence_sub_types\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||
\`contract_id\` INT NOT NULL COMMENT 'ผูกกับสัญญา',
|
||||
\`correspondence_type_id\` INT NOT NULL COMMENT 'ผูกกับประเภทเอกสารหลัก (เช่น RFA)',
|
||||
\`sub_type_code\` VARCHAR(20) NOT NULL COMMENT 'รหัสย่อย (เช่น MAT, SHP)',
|
||||
\`sub_type_name\` VARCHAR(255) COMMENT 'ชื่อประเภทหนังสือย่อย',
|
||||
\`sub_type_number\` VARCHAR(10) COMMENT 'เลขรหัสสำหรับ Running Number (เช่น 11, 22)',
|
||||
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (\`contract_id\`) REFERENCES \`contracts\` (\`id\`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (\`correspondence_type_id\`) REFERENCES \`correspondence_types\` (\`id\`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บประเภทหนังสือย่อย (Sub Types) ตาม Req 6B';
|
||||
`);
|
||||
|
||||
// 3. Add discipline_id to correspondences
|
||||
const hasDisciplineCol = await queryRunner.hasColumn(
|
||||
'correspondences',
|
||||
'discipline_id'
|
||||
);
|
||||
if (!hasDisciplineCol) {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`correspondences\`
|
||||
ADD COLUMN \`discipline_id\` INT NULL COMMENT 'สาขางาน (ถ้ามี)' AFTER \`correspondence_type_id\`,
|
||||
ADD CONSTRAINT \`fk_corr_discipline\` FOREIGN KEY (\`discipline_id\`) REFERENCES \`disciplines\` (\`id\`) ON DELETE SET NULL;
|
||||
`);
|
||||
}
|
||||
|
||||
// 4. Add discipline_id to rfas
|
||||
const hasRfaDisciplineCol = await queryRunner.hasColumn(
|
||||
'rfas',
|
||||
'discipline_id'
|
||||
);
|
||||
if (!hasRfaDisciplineCol) {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`rfas\`
|
||||
ADD COLUMN \`discipline_id\` INT NULL COMMENT 'สาขางาน (ถ้ามี)' AFTER \`rfa_type_id\`,
|
||||
ADD CONSTRAINT \`fk_rfa_discipline\` FOREIGN KEY (\`discipline_id\`) REFERENCES \`disciplines\` (\`id\`) ON DELETE SET NULL;
|
||||
`);
|
||||
}
|
||||
|
||||
// 5. Create Document Numbering Audit & Errors
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`document_number_audit\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
\`project_id\` INT NOT NULL,
|
||||
\`correspondence_type_id\` INT NOT NULL,
|
||||
\`running_number\` INT NOT NULL,
|
||||
\`full_document_number\` VARCHAR(100) NOT NULL,
|
||||
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
\`created_by\` INT NULL
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`document_number_errors\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
\`error_code\` VARCHAR(50),
|
||||
\`error_message\` TEXT,
|
||||
\`context_data\` JSON,
|
||||
\`occurred_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
// 6. Create RFA Items (Linking RFA to Shop Drawings)
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`rfa_items\` (
|
||||
\`rfarev_correspondence_id\` INT COMMENT 'ID ของ RFA Revision',
|
||||
\`shop_drawing_revision_id\` INT COMMENT 'ID ของ Shop Drawing Revision',
|
||||
PRIMARY KEY (\`rfarev_correspondence_id\`, \`shop_drawing_revision_id\`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
// 7. Create Transmittal Tables
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`transmittals\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||
\`correspondence_id\` INT UNIQUE COMMENT 'ID ของเอกสาร',
|
||||
\`transmittal_no\` VARCHAR(100) NOT NULL COMMENT 'เลขที่ใบนำส่ง',
|
||||
\`subject\` VARCHAR(500) NOT NULL COMMENT 'เรื่อง',
|
||||
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (\`correspondence_id\`) REFERENCES \`correspondences\` (\`id\`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`transmittal_items\` (
|
||||
\`transmittal_id\` INT NOT NULL,
|
||||
\`item_type\` VARCHAR(50) NOT NULL COMMENT 'ประเภทรายการ (DRAWING, RFA, etc.)',
|
||||
\`item_id\` INT NOT NULL COMMENT 'ID ของรายการ',
|
||||
\`description\` TEXT,
|
||||
PRIMARY KEY (\`transmittal_id\`, \`item_type\`, \`item_id\`),
|
||||
FOREIGN KEY (\`transmittal_id\`) REFERENCES \`transmittals\` (\`id\`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
// 8. Create Circulation Tables
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`circulation_status_codes\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
\`code\` VARCHAR(20) NOT NULL UNIQUE,
|
||||
\`description\` VARCHAR(50) NOT NULL,
|
||||
\`sort_order\` INT DEFAULT 0,
|
||||
\`is_active\` TINYINT(1) DEFAULT 1
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`circulations\` (
|
||||
\`id\` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
\`correspondence_id\` INT UNIQUE,
|
||||
\`organization_id\` INT NOT NULL,
|
||||
\`circulation_no\` VARCHAR(100) NOT NULL,
|
||||
\`circulation_subject\` VARCHAR(500) NOT NULL,
|
||||
\`circulation_status_code\` VARCHAR(20) NOT NULL,
|
||||
\`created_by_user_id\` INT NOT NULL,
|
||||
\`submitted_at\` TIMESTAMP NULL,
|
||||
\`closed_at\` TIMESTAMP NULL,
|
||||
\`created_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
\`updated_at\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (\`correspondence_id\`) REFERENCES \`correspondences\` (\`id\`),
|
||||
FOREIGN KEY (\`organization_id\`) REFERENCES \`organizations\` (\`id\`),
|
||||
FOREIGN KEY (\`circulation_status_code\`) REFERENCES \`circulation_status_codes\` (\`code\`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`circulation_attachments\` (
|
||||
\`circulation_id\` INT NOT NULL,
|
||||
\`attachment_id\` INT NOT NULL,
|
||||
PRIMARY KEY (\`circulation_id\`, \`attachment_id\`),
|
||||
FOREIGN KEY (\`circulation_id\`) REFERENCES \`circulations\` (\`id\`) ON DELETE CASCADE
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Drop tables in reverse order
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS circulation_attachments`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS circulations`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS circulation_status_codes`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS transmittal_items`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS transmittals`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS rfa_items`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS document_number_errors`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS document_number_audit`);
|
||||
|
||||
// Remove columns
|
||||
const hasRfaDisciplineCol = await queryRunner.hasColumn(
|
||||
'rfas',
|
||||
'discipline_id'
|
||||
);
|
||||
if (hasRfaDisciplineCol) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE rfas DROP FOREIGN KEY fk_rfa_discipline`
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE rfas DROP COLUMN discipline_id`);
|
||||
}
|
||||
|
||||
const hasDisciplineCol = await queryRunner.hasColumn(
|
||||
'correspondences',
|
||||
'discipline_id'
|
||||
);
|
||||
if (hasDisciplineCol) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE correspondences DROP FOREIGN KEY fk_corr_discipline`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE correspondences DROP COLUMN discipline_id`
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS correspondence_sub_types`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS disciplines`);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Circulation } from './entities/circulation.entity';
|
||||
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module';
|
||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||
import { CirculationWorkflowService } from './circulation-workflow.service';
|
||||
import { CirculationController } from './circulation.controller';
|
||||
import { CirculationService } from './circulation.service';
|
||||
@@ -20,6 +21,7 @@ import { CirculationService } from './circulation.service';
|
||||
]),
|
||||
UserModule,
|
||||
WorkflowEngineModule,
|
||||
DocumentNumberingModule,
|
||||
],
|
||||
controllers: [CirculationController],
|
||||
providers: [CirculationService, CirculationWorkflowService],
|
||||
|
||||
@@ -5,14 +5,15 @@ import {
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, Not } from 'typeorm'; // เพิ่ม Not
|
||||
import { Repository, DataSource, Not } from 'typeorm';
|
||||
|
||||
import { Circulation } from './entities/circulation.entity';
|
||||
import { CirculationRouting } from './entities/circulation-routing.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { CreateCirculationDto } from './dto/create-circulation.dto';
|
||||
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto'; // Import ใหม่
|
||||
import { SearchCirculationDto } from './dto/search-circulation.dto'; // Import ใหม่
|
||||
import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto';
|
||||
import { SearchCirculationDto } from './dto/search-circulation.dto';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
|
||||
@Injectable()
|
||||
export class CirculationService {
|
||||
@@ -21,7 +22,8 @@ export class CirculationService {
|
||||
private circulationRepo: Repository<Circulation>,
|
||||
@InjectRepository(CirculationRouting)
|
||||
private routingRepo: Repository<CirculationRouting>,
|
||||
private dataSource: DataSource,
|
||||
private numberingService: DocumentNumberingService,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateCirculationDto, user: User) {
|
||||
@@ -34,8 +36,17 @@ export class CirculationService {
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// Generate No. (Mock Logic) -> ควรใช้ NumberingService จริงในอนาคต
|
||||
const circulationNo = `CIR-${Date.now()}`;
|
||||
// Generate No. using DocumentNumberingService (Type 900 - Circulation)
|
||||
const circulationNo = await this.numberingService.generateNextNumber({
|
||||
projectId: createDto.projectId || 0, // Use projectId from DTO or 0
|
||||
originatorId: user.primaryOrganizationId,
|
||||
typeId: 900, // Fixed Type ID for Circulation
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
TYPE_CODE: 'CIR',
|
||||
ORG_CODE: 'ORG',
|
||||
},
|
||||
});
|
||||
|
||||
const circulation = queryRunner.manager.create(Circulation, {
|
||||
organizationId: user.primaryOrganizationId,
|
||||
@@ -55,7 +66,7 @@ export class CirculationService {
|
||||
organizationId: user.primaryOrganizationId,
|
||||
assignedTo: userId,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
})
|
||||
);
|
||||
await queryRunner.manager.save(routings);
|
||||
}
|
||||
@@ -83,13 +94,6 @@ export class CirculationService {
|
||||
query.andWhere('c.statusCode = :status', { status });
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query.andWhere(
|
||||
'(c.circulationNo LIKE :search OR c.subject LIKE :search)',
|
||||
{ search: `%${search}%` },
|
||||
);
|
||||
}
|
||||
|
||||
query
|
||||
.orderBy('c.createdAt', 'DESC')
|
||||
.skip((page - 1) * limit)
|
||||
@@ -113,7 +117,7 @@ export class CirculationService {
|
||||
async updateRoutingStatus(
|
||||
routingId: number,
|
||||
dto: UpdateCirculationRoutingDto,
|
||||
user: User,
|
||||
user: User
|
||||
) {
|
||||
const routing = await this.routingRepo.findOne({
|
||||
where: { id: routingId },
|
||||
|
||||
@@ -12,6 +12,10 @@ export class CreateCirculationDto {
|
||||
@IsNotEmpty()
|
||||
correspondenceId!: number; // เอกสารต้นเรื่องที่จะเวียน
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
projectId?: number; // Project ID for Numbering
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
subject!: string; // หัวข้อเรื่อง (Subject)
|
||||
|
||||
@@ -11,31 +11,31 @@ import {
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, Like, In } from 'typeorm';
|
||||
|
||||
// Entities
|
||||
import { Correspondence } from './entities/correspondence.entity.js';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity.js';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity.js';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity.js';
|
||||
import { RoutingTemplate } from './entities/routing-template.entity.js';
|
||||
import { CorrespondenceRouting } from './entities/correspondence-routing.entity.js';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity.js';
|
||||
import { User } from '../user/entities/user.entity.js';
|
||||
// Entitie
|
||||
import { Correspondence } from './entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from './entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from './entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from './entities/correspondence-status.entity';
|
||||
import { RoutingTemplate } from './entities/routing-template.entity';
|
||||
import { CorrespondenceRouting } from './entities/correspondence-routing.entity';
|
||||
import { CorrespondenceReference } from './entities/correspondence-reference.entity';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
// DTOs
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js';
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
|
||||
import { AddReferenceDto } from './dto/add-reference.dto.js';
|
||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto.js';
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
|
||||
import { WorkflowActionDto } from './dto/workflow-action.dto';
|
||||
import { AddReferenceDto } from './dto/add-reference.dto';
|
||||
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto';
|
||||
|
||||
// Interfaces & Enums
|
||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface.js';
|
||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface';
|
||||
|
||||
// Services
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
|
||||
import { JsonSchemaService } from '../json-schema/json-schema.service.js';
|
||||
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { SearchService } from '../search/search.service.js';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
import { JsonSchemaService } from '../json-schema/json-schema.service';
|
||||
import { WorkflowEngineService } from '../workflow-engine/workflow-engine.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { SearchService } from '../search/search.service';
|
||||
|
||||
@Injectable()
|
||||
export class CorrespondenceService {
|
||||
@@ -62,7 +62,7 @@ export class CorrespondenceService {
|
||||
private workflowEngine: WorkflowEngineService,
|
||||
private userService: UserService,
|
||||
private dataSource: DataSource,
|
||||
private searchService: SearchService,
|
||||
private searchService: SearchService
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateCorrespondenceDto, user: User) {
|
||||
@@ -76,7 +76,7 @@ export class CorrespondenceService {
|
||||
});
|
||||
if (!statusDraft) {
|
||||
throw new InternalServerErrorException(
|
||||
'Status DRAFT not found in Master Data',
|
||||
'Status DRAFT not found in Master Data'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,11 +92,11 @@ export class CorrespondenceService {
|
||||
// Impersonation Logic
|
||||
if (createDto.originatorId && createDto.originatorId !== userOrgId) {
|
||||
const permissions = await this.userService.getUserPermissions(
|
||||
user.user_id,
|
||||
user.user_id
|
||||
);
|
||||
if (!permissions.includes('system.manage_all')) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to create documents on behalf of other organizations.',
|
||||
'You do not have permission to create documents on behalf of other organizations.'
|
||||
);
|
||||
}
|
||||
userOrgId = createDto.originatorId;
|
||||
@@ -104,7 +104,7 @@ export class CorrespondenceService {
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create documents',
|
||||
'User must belong to an organization to create documents'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export class CorrespondenceService {
|
||||
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
|
||||
} catch (error: any) {
|
||||
this.logger.warn(
|
||||
`Schema validation warning for ${type.typeCode}: ${error.message}`,
|
||||
`Schema validation warning for ${type.typeCode}: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ export class Correspondence {
|
||||
@Column({ name: 'correspondence_type_id' })
|
||||
correspondenceTypeId!: number;
|
||||
|
||||
@Column({ name: 'discipline_id', nullable: true })
|
||||
disciplineId?: number;
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
projectId!: number;
|
||||
|
||||
@@ -64,10 +67,15 @@ export class Correspondence {
|
||||
@JoinColumn({ name: 'created_by' })
|
||||
creator?: User;
|
||||
|
||||
// [New V1.5.1]
|
||||
@ManyToOne('Discipline')
|
||||
@JoinColumn({ name: 'discipline_id' })
|
||||
discipline?: any; // Use 'any' or import Discipline entity if available to avoid circular dependency issues if not careful, but better to import.
|
||||
|
||||
// One Correspondence has Many Revisions
|
||||
@OneToMany(
|
||||
() => CorrespondenceRevision,
|
||||
(revision) => revision.correspondence,
|
||||
(revision) => revision.correspondence
|
||||
)
|
||||
revisions?: CorrespondenceRevision[];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
ConflictException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
@@ -38,7 +36,7 @@ export class ShopDrawingService {
|
||||
@InjectRepository(Attachment)
|
||||
private attachmentRepo: Repository<Attachment>,
|
||||
private fileStorageService: FileStorageService,
|
||||
private dataSource: DataSource,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -51,7 +49,7 @@ export class ShopDrawingService {
|
||||
});
|
||||
if (exists) {
|
||||
throw new ConflictException(
|
||||
`Drawing number "${createDto.drawingNumber}" already exists.`,
|
||||
`Drawing number "${createDto.drawingNumber}" already exists.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +101,7 @@ export class ShopDrawingService {
|
||||
// 5. Commit Files
|
||||
if (createDto.attachmentIds?.length) {
|
||||
await this.fileStorageService.commit(
|
||||
createDto.attachmentIds.map(String),
|
||||
createDto.attachmentIds.map(String)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,7 +115,7 @@ export class ShopDrawingService {
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Failed to create shop drawing: ${(err as Error).message}`,
|
||||
`Failed to create shop drawing: ${(err as Error).message}`
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -130,7 +128,7 @@ export class ShopDrawingService {
|
||||
*/
|
||||
async createRevision(
|
||||
shopDrawingId: number,
|
||||
createDto: CreateShopDrawingRevisionDto,
|
||||
createDto: CreateShopDrawingRevisionDto
|
||||
) {
|
||||
const shopDrawing = await this.shopDrawingRepo.findOneBy({
|
||||
id: shopDrawingId,
|
||||
@@ -144,7 +142,7 @@ export class ShopDrawingService {
|
||||
});
|
||||
if (exists) {
|
||||
throw new ConflictException(
|
||||
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`,
|
||||
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,7 +186,7 @@ export class ShopDrawingService {
|
||||
|
||||
if (createDto.attachmentIds?.length) {
|
||||
await this.fileStorageService.commit(
|
||||
createDto.attachmentIds.map(String),
|
||||
createDto.attachmentIds.map(String)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,7 +235,7 @@ export class ShopDrawingService {
|
||||
qb.where('sd.drawingNumber LIKE :search', {
|
||||
search: `%${search}%`,
|
||||
}).orWhere('sd.title LIKE :search', { search: `%${search}%` });
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
70
backend/src/modules/project/contract.controller.ts
Normal file
70
backend/src/modules/project/contract.controller.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiBearerAuth,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { ContractService } from './contract.service.js';
|
||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
@ApiTags('Contracts')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('contracts')
|
||||
export class ContractController {
|
||||
constructor(private readonly contractService: ContractService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Create Contract' })
|
||||
create(@Body() dto: CreateContractDto) {
|
||||
return this.contractService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'Get All Contracts (Optional: filter by projectId)',
|
||||
})
|
||||
@ApiQuery({ name: 'projectId', required: false, type: Number })
|
||||
findAll(@Query('projectId') projectId?: number) {
|
||||
return this.contractService.findAll(projectId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Contract by ID' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.contractService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Update Contract' })
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: UpdateContractDto
|
||||
) {
|
||||
return this.contractService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Delete Contract' })
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.contractService.remove(id);
|
||||
}
|
||||
}
|
||||
65
backend/src/modules/project/contract.service.ts
Normal file
65
backend/src/modules/project/contract.service.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Contract } from './entities/contract.entity.js';
|
||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class ContractService {
|
||||
constructor(
|
||||
@InjectRepository(Contract)
|
||||
private readonly contractRepo: Repository<Contract>
|
||||
) {}
|
||||
|
||||
async create(dto: CreateContractDto) {
|
||||
const existing = await this.contractRepo.findOne({
|
||||
where: { contractCode: dto.contractCode },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Contract Code "${dto.contractCode}" already exists`
|
||||
);
|
||||
}
|
||||
const contract = this.contractRepo.create(dto);
|
||||
return this.contractRepo.save(contract);
|
||||
}
|
||||
|
||||
async findAll(projectId?: number) {
|
||||
const query = this.contractRepo
|
||||
.createQueryBuilder('c')
|
||||
.leftJoinAndSelect('c.project', 'p')
|
||||
.orderBy('c.contractCode', 'ASC');
|
||||
|
||||
if (projectId) {
|
||||
query.where('c.projectId = :projectId', { projectId });
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const contract = await this.contractRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['project'],
|
||||
});
|
||||
if (!contract) throw new NotFoundException(`Contract ID ${id} not found`);
|
||||
return contract;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateContractDto) {
|
||||
const contract = await this.findOne(id);
|
||||
Object.assign(contract, dto);
|
||||
return this.contractRepo.save(contract);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const contract = await this.findOne(id);
|
||||
// Schema doesn't have deleted_at for Contract either.
|
||||
return this.contractRepo.remove(contract);
|
||||
}
|
||||
}
|
||||
49
backend/src/modules/project/dto/create-contract.dto.ts
Normal file
49
backend/src/modules/project/dto/create-contract.dto.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
Length,
|
||||
IsInt,
|
||||
IsDateString,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateContractDto {
|
||||
@ApiProperty({ example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number;
|
||||
|
||||
@ApiProperty({ example: 'C-001' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 50)
|
||||
contractCode!: string;
|
||||
|
||||
@ApiProperty({ example: 'Main Construction Contract' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 255)
|
||||
contractName!: string;
|
||||
|
||||
@ApiProperty({ example: 'Description of the contract', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: '2024-01-01', required: false })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startDate?: string; // Receive as string, TypeORM handles date
|
||||
|
||||
@ApiProperty({ example: '2025-12-31', required: false })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endDate?: string;
|
||||
|
||||
@ApiProperty({ example: true, required: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
27
backend/src/modules/project/dto/create-organization.dto.ts
Normal file
27
backend/src/modules/project/dto/create-organization.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
Length,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateOrganizationDto {
|
||||
@ApiProperty({ example: 'ITD' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 20)
|
||||
organizationCode!: string;
|
||||
|
||||
@ApiProperty({ example: 'Italian-Thai Development' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 255)
|
||||
organizationName!: string;
|
||||
|
||||
@ApiProperty({ example: true, required: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
4
backend/src/modules/project/dto/update-contract.dto.ts
Normal file
4
backend/src/modules/project/dto/update-contract.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateContractDto } from './create-contract.dto.js';
|
||||
|
||||
export class UpdateContractDto extends PartialType(CreateContractDto) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateOrganizationDto } from './create-organization.dto.js';
|
||||
|
||||
export class UpdateOrganizationDto extends PartialType(CreateOrganizationDto) {}
|
||||
61
backend/src/modules/project/organization.controller.ts
Normal file
61
backend/src/modules/project/organization.controller.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { OrganizationService } from './organization.service.js';
|
||||
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
@ApiTags('Organizations')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('organizations')
|
||||
export class OrganizationController {
|
||||
constructor(private readonly orgService: OrganizationService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Create Organization' })
|
||||
create(@Body() dto: CreateOrganizationDto) {
|
||||
return this.orgService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get All Organizations' })
|
||||
findAll() {
|
||||
return this.orgService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Organization by ID' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.orgService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Update Organization' })
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: UpdateOrganizationDto
|
||||
) {
|
||||
return this.orgService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Delete Organization' })
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.orgService.remove(id);
|
||||
}
|
||||
}
|
||||
57
backend/src/modules/project/organization.service.ts
Normal file
57
backend/src/modules/project/organization.service.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationService {
|
||||
constructor(
|
||||
@InjectRepository(Organization)
|
||||
private readonly orgRepo: Repository<Organization>
|
||||
) {}
|
||||
|
||||
async create(dto: CreateOrganizationDto) {
|
||||
const existing = await this.orgRepo.findOne({
|
||||
where: { organizationCode: dto.organizationCode },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Organization Code "${dto.organizationCode}" already exists`
|
||||
);
|
||||
}
|
||||
const org = this.orgRepo.create(dto);
|
||||
return this.orgRepo.save(org);
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.orgRepo.find({
|
||||
order: { organizationCode: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const org = await this.orgRepo.findOne({ where: { id } });
|
||||
if (!org) throw new NotFoundException(`Organization ID ${id} not found`);
|
||||
return org;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateOrganizationDto) {
|
||||
const org = await this.findOne(id);
|
||||
Object.assign(org, dto);
|
||||
return this.orgRepo.save(org);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const org = await this.findOne(id);
|
||||
// Hard delete or Soft delete? Schema doesn't have deleted_at for Organization, but let's check.
|
||||
// Schema says: created_at, updated_at. No deleted_at.
|
||||
// So hard delete.
|
||||
return this.orgRepo.remove(org);
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,32 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ProjectService } from './project.service.js';
|
||||
import { ProjectController } from './project.controller.js';
|
||||
import { OrganizationService } from './organization.service.js';
|
||||
import { OrganizationController } from './organization.controller.js';
|
||||
import { ContractService } from './contract.service.js';
|
||||
import { ContractController } from './contract.controller.js';
|
||||
|
||||
import { Project } from './entities/project.entity.js';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
import { Contract } from './entities/contract.entity.js';
|
||||
import { ProjectOrganization } from './entities/project-organization.entity.js'; // เพิ่ม
|
||||
import { ContractOrganization } from './entities/contract-organization.entity.js'; // เพิ่ม
|
||||
import { ProjectOrganization } from './entities/project-organization.entity.js';
|
||||
import { ContractOrganization } from './entities/contract-organization.entity.js';
|
||||
// Modules
|
||||
import { UserModule } from '../user/user.module'; // ✅ 1. Import UserModule
|
||||
import { UserModule } from '../user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Project,
|
||||
Organization,
|
||||
Contract,
|
||||
ProjectOrganization, // ลงทะเบียน
|
||||
ContractOrganization, // ลงทะเบียน
|
||||
ProjectOrganization,
|
||||
ContractOrganization,
|
||||
]),
|
||||
UserModule, // ✅ 2. เพิ่ม UserModule เข้าไปใน imports
|
||||
UserModule,
|
||||
],
|
||||
controllers: [ProjectController],
|
||||
providers: [ProjectService],
|
||||
exports: [ProjectService], // Export เผื่อ Module อื่นใช้
|
||||
controllers: [ProjectController, OrganizationController, ContractController],
|
||||
providers: [ProjectService, OrganizationService, ContractService],
|
||||
exports: [ProjectService, OrganizationService, ContractService],
|
||||
})
|
||||
export class ProjectModule {}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto';
|
||||
import { CreateRfaDto } from './dto/create-rfa.dto';
|
||||
|
||||
// Interfaces & Enums
|
||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface';
|
||||
|
||||
// Services
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
@@ -68,7 +69,7 @@ export class RfaService {
|
||||
private workflowEngine: WorkflowEngineService,
|
||||
private notificationService: NotificationService,
|
||||
private dataSource: DataSource,
|
||||
private searchService: SearchService,
|
||||
private searchService: SearchService
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateRfaDto, user: User) {
|
||||
@@ -82,7 +83,7 @@ export class RfaService {
|
||||
});
|
||||
if (!statusDraft) {
|
||||
throw new InternalServerErrorException(
|
||||
'Status DFT (Draft) not found in Master Data',
|
||||
'Status DFT (Draft) not found in Master Data'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,24 +120,20 @@ export class RfaService {
|
||||
// 1. Create Correspondence Record
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber,
|
||||
correspondenceTypeId: createDto.rfaTypeId, // Assuming RFA Type maps directly or via logic
|
||||
// Note: ถ้า CorrespondenceType แยก ID กับ RFA Type ต้อง Map ให้ถูก
|
||||
// ในที่นี้สมมติว่าใช้ ID เดียวกัน หรือ RFA Type เป็น SubType ของ Correspondence
|
||||
correspondenceTypeId: createDto.rfaTypeId,
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
isInternalCommunication: false,
|
||||
isInternal: false,
|
||||
createdBy: user.user_id,
|
||||
// ✅ Add disciplineId column if correspondence table supports it (as per Data Dictionary Update)
|
||||
// disciplineId: createDto.disciplineId
|
||||
disciplineId: createDto.disciplineId, // ✅ Add disciplineId
|
||||
});
|
||||
const savedCorr = await queryRunner.manager.save(correspondence);
|
||||
|
||||
// 2. Create RFA Master Record
|
||||
// 2. Create Rfa Master Record
|
||||
const rfa = queryRunner.manager.create(Rfa, {
|
||||
rfaTypeId: createDto.rfaTypeId,
|
||||
createdBy: user.user_id,
|
||||
// ✅ ถ้า Entity Rfa มี disciplineId ให้ใส่ตรงนี้ด้วย
|
||||
// disciplineId: createDto.disciplineId
|
||||
disciplineId: createDto.disciplineId, // ✅ Add disciplineId
|
||||
});
|
||||
const savedRfa = await queryRunner.manager.save(rfa);
|
||||
|
||||
@@ -154,8 +151,8 @@ export class RfaService {
|
||||
? new Date(createDto.documentDate)
|
||||
: new Date(),
|
||||
createdBy: user.user_id,
|
||||
details: createDto.details, // ✅ Save JSON Details
|
||||
schemaVersion: 1, // ✅ Default Schema Version
|
||||
details: createDto.details,
|
||||
schemaVersion: 1,
|
||||
});
|
||||
const savedRevision = await queryRunner.manager.save(rfaRevision);
|
||||
|
||||
@@ -174,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;
|
||||
|
||||
@@ -4,11 +4,12 @@ import {
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
ValidateNested,
|
||||
IsEnum,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
// Enum นี้ควรตรงกับใน Entity หรือสร้างไฟล์ enum แยก (ในที่นี้ใส่ไว้ใน DTO เพื่อความสะดวก)
|
||||
export enum TransmittalPurpose {
|
||||
FOR_APPROVAL = 'FOR_APPROVAL',
|
||||
FOR_INFORMATION = 'FOR_INFORMATION',
|
||||
@@ -16,21 +17,57 @@ export enum TransmittalPurpose {
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export class CreateTransmittalDto {
|
||||
export class TransmittalItemDto {
|
||||
@ApiProperty({
|
||||
description: 'ประเภทรายการ (DRAWING, RFA, CORRESPONDENCE)',
|
||||
example: 'DRAWING',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
itemType!: string;
|
||||
|
||||
@ApiProperty({ description: 'ID ของรายการ', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number; // จำเป็นสำหรับการออกเลขที่เอกสาร (Running Number)
|
||||
|
||||
@IsEnum(TransmittalPurpose)
|
||||
@IsOptional()
|
||||
purpose?: TransmittalPurpose; // วัตถุประสงค์การส่ง
|
||||
itemId!: number;
|
||||
|
||||
@ApiProperty({ description: 'รายละเอียดเพิ่มเติม', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
remarks?: string; // หมายเหตุเพิ่มเติม
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@IsNotEmpty()
|
||||
itemIds!: number[]; // ID ของเอกสาร (Correspondence IDs) ที่จะแนบไปใน Transmittal นี้
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class CreateTransmittalDto {
|
||||
@ApiProperty({ description: 'ID ของโครงการ', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'เรื่อง',
|
||||
example: 'Transmittal for Shop Drawings',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
subject!: string;
|
||||
|
||||
@ApiProperty({ description: 'ผู้รับ (Organization ID)', example: 2 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
recipientOrganizationId!: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'วัตถุประสงค์',
|
||||
enum: TransmittalPurpose,
|
||||
example: TransmittalPurpose.FOR_APPROVAL,
|
||||
})
|
||||
@IsEnum(TransmittalPurpose)
|
||||
@IsOptional()
|
||||
purpose?: TransmittalPurpose;
|
||||
|
||||
@ApiProperty({ description: 'รายการที่แนบ', type: [TransmittalItemDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TransmittalItemDto)
|
||||
items!: TransmittalItemDto[];
|
||||
}
|
||||
|
||||
@@ -1,36 +1,22 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Entity, Column, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
import { Transmittal } from './transmittal.entity';
|
||||
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
||||
|
||||
@Entity('transmittal_items')
|
||||
export class TransmittalItem {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'transmittal_id' })
|
||||
@PrimaryColumn({ name: 'transmittal_id' })
|
||||
transmittalId!: number;
|
||||
|
||||
@Column({ name: 'item_correspondence_id' })
|
||||
itemCorrespondenceId!: number;
|
||||
@PrimaryColumn({ name: 'item_type', length: 50 })
|
||||
itemType!: string; // DRAWING, RFA, etc.
|
||||
|
||||
@Column({ default: 1 })
|
||||
quantity!: number;
|
||||
@PrimaryColumn({ name: 'item_id' })
|
||||
itemId!: number;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
remarks?: string;
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Transmittal, (t) => t.items, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'transmittal_id' })
|
||||
transmittal!: Transmittal;
|
||||
|
||||
@ManyToOne(() => Correspondence)
|
||||
@JoinColumn({ name: 'item_correspondence_id' })
|
||||
itemDocument!: Correspondence;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
||||
import { TransmittalItem } from './transmittal-item.entity';
|
||||
|
||||
@Entity('transmittals')
|
||||
export class Transmittal {
|
||||
@PrimaryColumn({ name: 'correspondence_id' })
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'correspondence_id', unique: true })
|
||||
correspondenceId!: number;
|
||||
|
||||
@Column({ name: 'transmittal_no', length: 100 })
|
||||
transmittalNo!: string;
|
||||
|
||||
@Column({ length: 500 })
|
||||
subject!: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['FOR_APPROVAL', 'FOR_INFORMATION', 'FOR_REVIEW', 'OTHER'],
|
||||
@@ -24,6 +34,9 @@ export class Transmittal {
|
||||
@Column({ type: 'text', nullable: true })
|
||||
remarks?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
// Relations
|
||||
@OneToOne(() => Correspondence)
|
||||
@JoinColumn({ name: 'correspondence_id' })
|
||||
|
||||
@@ -4,50 +4,32 @@ import {
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { TransmittalService } from './transmittal.service';
|
||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
|
||||
import { SearchTransmittalDto } from './dto/search-transmittal.dto'; // เดี๋ยวสร้าง DTO นี้เพิ่มให้ครับถ้ายังไม่มี
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('Transmittals')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('transmittals')
|
||||
export class TransmittalController {
|
||||
constructor(private readonly transmittalService: TransmittalService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create new Transmittal' })
|
||||
@RequirePermission('transmittal.create') // สิทธิ์ ID 40
|
||||
@Audit('transmittal.create', 'transmittal') // ✅ แปะตรงนี้
|
||||
@ApiOperation({ summary: 'Create a new Transmittal' })
|
||||
create(@Body() createDto: CreateTransmittalDto, @CurrentUser() user: User) {
|
||||
return this.transmittalService.create(createDto, user);
|
||||
}
|
||||
|
||||
// เพิ่ม Endpoint พื้นฐานสำหรับการค้นหา (Optional)
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Search Transmittals' })
|
||||
@RequirePermission('document.view')
|
||||
findAll(@Query() searchDto: SearchTransmittalDto) {
|
||||
// return this.transmittalService.findAll(searchDto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Transmittal details' })
|
||||
@RequirePermission('document.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
// return this.transmittalService.findOne(id);
|
||||
return this.transmittalService.findOne(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,23 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Transmittal } from './entities/transmittal.entity';
|
||||
import { TransmittalItem } from './entities/transmittal-item.entity';
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
||||
import { TransmittalService } from './transmittal.service';
|
||||
import { TransmittalController } from './transmittal.controller';
|
||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { SearchModule } from '../search/search.module'; // ✅ ต้อง Import เพราะ Service ใช้ (ที่เป็นสาเหตุ Error)
|
||||
import { SearchModule } from '../search/search.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Transmittal, TransmittalItem, Correspondence]),
|
||||
TypeOrmModule.forFeature([
|
||||
Transmittal,
|
||||
TransmittalItem,
|
||||
Correspondence,
|
||||
CorrespondenceType,
|
||||
CorrespondenceStatus,
|
||||
]),
|
||||
DocumentNumberingModule,
|
||||
UserModule,
|
||||
SearchModule,
|
||||
|
||||
@@ -1,98 +1,143 @@
|
||||
// File: src/modules/transmittal/transmittal.service.ts
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
InternalServerErrorException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, In } from 'typeorm';
|
||||
|
||||
import { Transmittal } from './entities/transmittal.entity.js';
|
||||
import { TransmittalItem } from './entities/transmittal-item.entity.js';
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity.js';
|
||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto.js';
|
||||
import { User } from '../user/entities/user.entity.js';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
|
||||
import { SearchService } from '../search/search.service.js';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { Transmittal } from './entities/transmittal.entity';
|
||||
import { TransmittalItem } from './entities/transmittal-item.entity';
|
||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TransmittalService {
|
||||
private readonly logger = new Logger(TransmittalService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Transmittal)
|
||||
private transmittalRepo: Repository<Transmittal>,
|
||||
@InjectRepository(TransmittalItem)
|
||||
private transmittalItemRepo: Repository<TransmittalItem>,
|
||||
@InjectRepository(Correspondence)
|
||||
private correspondenceRepo: Repository<Correspondence>,
|
||||
private itemRepo: Repository<TransmittalItem>,
|
||||
@InjectRepository(CorrespondenceType)
|
||||
private typeRepo: Repository<CorrespondenceType>,
|
||||
@InjectRepository(CorrespondenceStatus)
|
||||
private statusRepo: Repository<CorrespondenceStatus>,
|
||||
private numberingService: DocumentNumberingService,
|
||||
private dataSource: DataSource,
|
||||
private searchService: SearchService,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateTransmittalDto, user: User) {
|
||||
if (!user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create documents',
|
||||
);
|
||||
}
|
||||
const userOrgId = user.primaryOrganizationId;
|
||||
// 1. Get Transmittal Type (Assuming Code '901' or 'TRN')
|
||||
const type = await this.typeRepo.findOne({
|
||||
where: { typeCode: 'TRN' }, // Adjust code as per Master Data
|
||||
});
|
||||
if (!type) throw new NotFoundException('Transmittal Type (TRN) not found');
|
||||
|
||||
const statusDraft = await this.statusRepo.findOne({
|
||||
where: { statusCode: 'DRAFT' },
|
||||
});
|
||||
if (!statusDraft)
|
||||
throw new InternalServerErrorException('Status DRAFT not found');
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const transmittalTypeId = 3; // TODO: ดึง ID จริงจาก DB หรือ Config
|
||||
const orgCode = 'ORG'; // TODO: Fetch real ORG Code
|
||||
if (!user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create a transmittal'
|
||||
);
|
||||
}
|
||||
|
||||
// [FIXED] เรียกใช้แบบ Object Context
|
||||
try {
|
||||
// 2. Generate Number
|
||||
const docNumber = await this.numberingService.generateNextNumber({
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
typeId: transmittalTypeId,
|
||||
originatorId: user.primaryOrganizationId,
|
||||
typeId: type.id,
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
TYPE_CODE: 'TR',
|
||||
ORG_CODE: orgCode,
|
||||
TYPE_CODE: type.typeCode,
|
||||
ORG_CODE: 'ORG', // TODO: Fetch real ORG Code
|
||||
},
|
||||
});
|
||||
|
||||
// 3. Create Correspondence (Parent)
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber,
|
||||
correspondenceTypeId: transmittalTypeId,
|
||||
correspondenceTypeId: type.id,
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
originatorId: user.primaryOrganizationId,
|
||||
isInternal: false,
|
||||
createdBy: user.user_id,
|
||||
});
|
||||
const savedCorr = await queryRunner.manager.save(correspondence);
|
||||
|
||||
// 4. Create Revision (Draft)
|
||||
const revision = queryRunner.manager.create(CorrespondenceRevision, {
|
||||
correspondenceId: savedCorr.id,
|
||||
revisionNumber: 0,
|
||||
revisionLabel: '0',
|
||||
isCurrent: true,
|
||||
statusId: statusDraft.id,
|
||||
title: createDto.subject,
|
||||
createdBy: user.user_id,
|
||||
});
|
||||
await queryRunner.manager.save(revision);
|
||||
|
||||
// 5. Create Transmittal
|
||||
const transmittal = queryRunner.manager.create(Transmittal, {
|
||||
correspondenceId: savedCorr.id,
|
||||
purpose: createDto.purpose,
|
||||
remarks: createDto.remarks,
|
||||
transmittalNo: docNumber,
|
||||
subject: createDto.subject,
|
||||
});
|
||||
await queryRunner.manager.save(transmittal);
|
||||
const savedTransmittal = await queryRunner.manager.save(transmittal);
|
||||
|
||||
if (createDto.itemIds && createDto.itemIds.length > 0) {
|
||||
const items = createDto.itemIds.map((itemId) =>
|
||||
// 6. Create Items
|
||||
if (createDto.items && createDto.items.length > 0) {
|
||||
const items = createDto.items.map((item) =>
|
||||
queryRunner.manager.create(TransmittalItem, {
|
||||
transmittalId: savedCorr.id,
|
||||
itemCorrespondenceId: itemId,
|
||||
quantity: 1,
|
||||
}),
|
||||
transmittalId: savedTransmittal.id,
|
||||
itemType: item.itemType,
|
||||
itemId: item.itemId,
|
||||
description: item.description,
|
||||
})
|
||||
);
|
||||
await queryRunner.manager.save(items);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { ...savedCorr, transmittal };
|
||||
|
||||
return {
|
||||
...savedTransmittal,
|
||||
correspondence: savedCorr,
|
||||
};
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Failed to create transmittal: ${(err as Error).message}`
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const transmittal = await this.transmittalRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['correspondence', 'items'],
|
||||
});
|
||||
if (!transmittal)
|
||||
throw new NotFoundException(`Transmittal ID ${id} not found`);
|
||||
return transmittal;
|
||||
}
|
||||
}
|
||||
911
docs/0_Requirements_V1_5_1.md
Normal file
911
docs/0_Requirements_V1_5_1.md
Normal 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. Non‑Functional Requirements
|
||||
|
||||
# 4.1 Access Control
|
||||
# 🔐 Section 4: Access Control (ข้อกำหนดด้านสิทธิ์และการเข้าถึง)
|
||||
|
||||
## 4.1. Overview:
|
||||
|
||||
- Users and organizations can view and edit documents based on the permissions they have. The system's permissions will be based on Role-Based Access Control (RBAC).
|
||||
|
||||
## 4.2. Permission Hierarchy:
|
||||
|
||||
- Global: The highest level of permissions in the system
|
||||
- Organization: Permissions within an organization, which is the basic permission for users
|
||||
- Project: Permissions specific to a project, which will be considered when the user is in that project
|
||||
- Contract: Permissions specific to a contract, which will be considered when the user is in that contract
|
||||
|
||||
## 4.3. Permission Enforcement:
|
||||
|
||||
- When checking permissions, the system will consider permissions from all levels that the user has and use the most permissive permission as the decision
|
||||
- Example: User A is a Viewer in the organization, but is assigned as an Editor in Project X when in Project X, User A will have the right to edit
|
||||
|
||||
## 4.4. Role and Scope:
|
||||
|
||||
| Role | Scope | Description | Key Permissions |
|
||||
| :------------------- | :----------- | :------------------------- | :-------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Superadmin** | Global | System administrator | Do everything in the system, manage organizations, manage global data |
|
||||
| **Org Admin** | Organization | Organization administrator | Manage users in the organization, manage roles/permissions within the organization, view organization reports |
|
||||
| **Document Control** | Organization | Document controller | Add/edit/delete documents, set document permissions within the organization |
|
||||
| **Editor** | Organization | Document editor | Edit documents that have been assigned to them |
|
||||
| **Viewer** | Organization | Document viewer | View documents that have access permissions |
|
||||
| **Project Manager** | Project | Project manager | Manage members in the project (add/delete/assign roles), create/manage contracts in the project, view project reports |
|
||||
| **Contract Admin** | Contract | Contract administrator | Manage users in the contract, manage roles/permissions within the contract, view contract reports |
|
||||
|
||||
## 4.5. Token Management (ปรับปรุง)
|
||||
|
||||
- **Payload Optimization:** ใน JWT Access Token ให้เก็บเฉพาะ `userId` และ `scope` ปัจจุบันเท่านั้น
|
||||
- **Permission Caching:** สิทธิ์ละเอียด (Permissions List) ให้เก็บใน **Redis** และดึงมาตรวจสอบเมื่อ Request เข้ามา เพื่อลดขนาด Token และเพิ่มความเร็ว
|
||||
|
||||
## 4.6. Onboarding Workflow
|
||||
|
||||
- 4.6.1. Create Organization
|
||||
- **Superadmin** creates a new organization (e.g. Company A)
|
||||
- **Superadmin** appoints at least 1 user as **Org Admin** or **Document Control** of Company A
|
||||
- 4.6.2. Add Users to Organization
|
||||
- **Org Admin** of Company A adds other users (Editor, Viewer) to the organization
|
||||
- 4.6.3. Assign Users to Project
|
||||
- **Project Manager** of Project X (which may come from Company A or another company) invites or assigns users from different organizations to join Project X
|
||||
- In this step, **Project Manager** will assign **Project Role** (e.g. Project Member, or may use organization-level permissions)
|
||||
- 4.6.4. Assign Users to Contract
|
||||
- **Contract Admin** of Contract Y (which is part of Project X) selects users from Project X and assigns them to Contract Y
|
||||
- In this step, **Contract Admin** will assign **Contract Role** (e.g. Contract Member) and specific permissions
|
||||
- 4.6.5 Security Onboarding:
|
||||
- Force users to change password for the first time
|
||||
- Security awareness training for users with high permissions
|
||||
- Safe password reset process
|
||||
- Audit log recording every permission change
|
||||
|
||||
### **4.7. Master Data Management**
|
||||
|
||||
| Master Data | Manager | Scope |
|
||||
| :-------------------------------------- | :------------------------------ | :------------------------------ |
|
||||
| Document Type (Correspondence, RFA) | **Superadmin** | Global |
|
||||
| Document Status (Draft, Approved, etc.) | **Superadmin** | Global |
|
||||
| Shop Drawing Category | **Project Manager** | Project (สร้างใหม่ได้ภายในโครงการ) |
|
||||
| Tags | **Org Admin / Project Manager** | Organization / Project |
|
||||
| Custom Roles | **Superadmin / Org Admin** | Global / Organization |
|
||||
| Document Numbering Formats | **Superadmin / Admin** | Global / Organization |
|
||||
|
||||
## 4.8. การบันทึกการกระทำ (Audit Log)
|
||||
|
||||
- ทุกการกระทำที่สำคัญของผู้ใช้ (สร้าง, แก้ไข, ลบ, ส่ง) จะถูกบันทึกไว้ใน audit_logs เพื่อการตรวจสอบย้อนหลัง
|
||||
- ขอบเขตการบันทึก Audit Log:
|
||||
- ทุกการสร้าง/แก้ไข/ลบ ข้อมูลสำคัญ (correspondences, RFAs, drawings, users, permissions)
|
||||
- ทุกการเข้าถึงข้อมูล sensitive (user data, financial information)
|
||||
- ทุกการเปลี่ยนสถานะ workflow (status transitions)
|
||||
- ทุกการดาวน์โหลดไฟล์สำคัญ (contract documents, financial reports)
|
||||
- ทุกการเปลี่ยนแปลง permission และ role assignment
|
||||
- ทุกการล็อกอินที่สำเร็จและล้มเหลว
|
||||
- ทุกการส่งคำขอ API ที่สำคัญ
|
||||
- ข้อมูลที่ต้องบันทึกใน Audit Log:
|
||||
- ผู้ใช้งาน (user_id)
|
||||
- การกระทำ (action)
|
||||
- ชนิดของ entity (entity_type)
|
||||
- ID ของ entity (entity_id)
|
||||
- ข้อมูลก่อนการเปลี่ยนแปลง (old_values) - สำหรับ update operations
|
||||
- ข้อมูลหลังการเปลี่ยนแปลง (new_values) - สำหรับ update operations
|
||||
- IP address
|
||||
- User agent
|
||||
- Timestamp
|
||||
- Request ID สำหรับ tracing
|
||||
|
||||
## 4.9. Data Archiving & Partitioning
|
||||
|
||||
- สำหรับตารางที่มีขนาดใหญ่และโตเร็ว (เช่น `audit_logs`, `notifications`, `correspondence_revisions`) ต้องออกแบบโดยรองรับ **Table Partitioning** (แบ่งตาม Range วันที่ หรือ List) เพื่อประสิทธิภาพในระยะยาว
|
||||
|
||||
## 4.10. การค้นหา (Search):
|
||||
|
||||
- ระบบต้องมีฟังก์ชันการค้นหาขั้นสูง ที่สามารถค้นหาเอกสาร **correspondence**, **rfa**, **shop_drawing**, **contract-drawing**, **transmittal** และ **ใบเวียน (Circulations)** จากหลายเงื่อนไขพร้อมกันได้ เช่น ค้นหาจากชื่อเรื่อง, ประเภท, วันที่, และ Tag
|
||||
|
||||
## 4.11. การทำรายงาน (Reporting):
|
||||
|
||||
- สามารถจัดทำรายงานสรุปแยกประเภทของ Correspondence ประจำวัน, สัปดาห์, เดือน, และปีได้
|
||||
|
||||
## 4.12. ประสิทธิภาพ (Performance):
|
||||
|
||||
- มีการใช้ Caching กับข้อมูลที่เรียกใช้บ่อย และใช้ Pagination ในตารางข้อมูลเพื่อจัดการข้อมูลจำนวนมาก
|
||||
|
||||
- ตัวชี้วัดประสิทธิภาพ:
|
||||
|
||||
- **API Response Time:** < 200ms (90th percentile) สำหรับ operation ทั่วไป
|
||||
- **Search Query Performance:** < 500ms สำหรับการค้นหาขั้นสูง
|
||||
- **File Upload Performance:** < 30 seconds สำหรับไฟล์ขนาด 50MB
|
||||
- **Concurrent Users:** รองรับผู้ใช้พร้อมกันอย่างน้อย 100 คน
|
||||
- **Database Connection Pool:** ขนาดเหมาะสมกับ workload (default: min 5, max 20 connections)
|
||||
- **Cache Hit Ratio:** > 80% สำหรับ cached data
|
||||
- **Application Startup Time:** < 30 seconds
|
||||
|
||||
- Caching Strategy:
|
||||
|
||||
- **Master Data Cache:** Roles, Permissions, Organizations, Project metadata (TTL: 1 hour)
|
||||
- **User Session Cache:** User permissions และ profile data (TTL: 30 minutes)
|
||||
- **Search Result Cache:** Frequently searched queries (TTL: 15 minutes)
|
||||
- **File Metadata Cache:** Attachment metadata (TTL: 1 hour)
|
||||
- **Document Cache:** Frequently accessed document metadata (TTL: 30 minutes)
|
||||
- **ต้องมี cache invalidation strategy ที่ชัดเจน:**
|
||||
- Invalidate on update/delete operations
|
||||
- Time-based expiration
|
||||
- Manual cache clearance สำหรับ admin operations
|
||||
- ใช้ Redis เป็น distributed cache backend
|
||||
- ต้องมี cache monitoring (hit/miss ratios)
|
||||
|
||||
- Frontend Performance:
|
||||
- **Bundle Size Optimization:** ต้องควบคุมขนาด Bundle โดยรวมไม่เกิน 2MB
|
||||
- **State Management Efficiency:** ใช้ State Management Libraries อย่างเหมาะสม ไม่เกิน 2 ตัวหลัก
|
||||
- **Memory Management:** ต้องป้องกัน Memory Leak จาก State ที่ไม่จำเป็น
|
||||
|
||||
## 4.13. System Security (ความปลอดภัยระบบ):
|
||||
|
||||
- มีระบบ Rate Limiting เพื่อป้องกันการโจมตีแบบ Brute-force
|
||||
- การจัดการ Secret (เช่น รหัสผ่าน DB, JWT Secret) จะต้องทำผ่าน Environment Variable ของ Docker เพื่อความปลอดภัยสูงสุด
|
||||
- Rate Limiting Strategy:
|
||||
- **Anonymous Endpoints:** 100 requests/hour ต่อ IP address
|
||||
- **Authenticated Endpoints:**
|
||||
- Viewer: 500 requests/hour
|
||||
- Editor: 1000 requests/hour
|
||||
- Document Control: 2000 requests/hour
|
||||
- Admin/Superadmin: 5000 requests/hour
|
||||
- **File Upload Endpoints:** 50 requests/hour ต่อ user
|
||||
- **Search Endpoints:** 500 requests/hour ต่อ user
|
||||
- **Authentication Endpoints:** 10 requests/minute ต่อ IP address
|
||||
- **ต้องมี mechanism สำหรับยกเว้น rate limiting สำหรับ trusted services**
|
||||
- ต้องบันทึก log เมื่อมีการ trigger rate limiting
|
||||
- Error Handling และ Resilience:
|
||||
- ต้องมี circuit breaker pattern สำหรับ external service calls
|
||||
- ต้องมี retry mechanism ด้วย exponential backoff
|
||||
- ต้องมี graceful degradation เมื่อบริการภายนอกล้มเหลว
|
||||
- Error messages ต้องไม่เปิดเผยข้อมูล sensitive
|
||||
- Input Validation:
|
||||
- ต้องมี input validation ทั้งฝั่ง client และ server (defense in depth)
|
||||
- ต้องป้องกัน OWASP Top 10 vulnerabilities:
|
||||
- SQL Injection (ใช้ parameterized queries ผ่าน ORM)
|
||||
- XSS (input sanitization และ output encoding)
|
||||
- CSRF (CSRF tokens สำหรับ state-changing operations)
|
||||
- ต้อง validate file uploads:
|
||||
- File type (white-list approach)
|
||||
- File size
|
||||
- File content (magic number verification)
|
||||
- ต้อง sanitize user inputs ก่อนแสดงผลใน UI
|
||||
- ต้องใช้ content security policy (CSP) headers
|
||||
- ต้องมี request size limits เพื่อป้องกัน DoS attacks
|
||||
- Session และ Token Management:
|
||||
- **JWT token expiration:** 8 hours สำหรับ access token
|
||||
- **Refresh token expiration:** 7 days
|
||||
- **Refresh token mechanism:** ต้องรองรับ token rotation และ revocation
|
||||
- **Token revocation on logout:** ต้องบันทึก revoked tokens จนกว่าจะ expire
|
||||
- **Concurrent session management:**
|
||||
- จำกัดจำนวน session พร้อมกันได้ (default: 5 devices)
|
||||
- ต้องแจ้งเตือนเมื่อมี login จาก device/location ใหม่
|
||||
- **Device fingerprinting:** สำหรับ security และ audit purposes
|
||||
- **Password policy:**
|
||||
- ความยาวขั้นต่ำ: 8 characters
|
||||
- ต้องมี uppercase, lowercase, number, special character
|
||||
- ต้องเปลี่ยน password ทุก 90 วัน
|
||||
- ต้องป้องกันการใช้ password ที่เคยใช้มาแล้ว 5 ครั้งล่าสุด
|
||||
|
||||
## 4.14. การสำรองข้อมูลและการกู้คืน (Backup & Recovery)
|
||||
|
||||
- ระบบจะต้องมีกลไกการสำรองข้อมูลอัตโนมัติสำหรับฐานข้อมูล MariaDB [cite: 2.4] และไฟล์เอกสารทั้งหมดใน /share/dms-data [cite: 2.1] (เช่น ใช้ HBS 3 ของ QNAP หรือสคริปต์สำรองข้อมูล) อย่างน้อยวันละ 1 ครั้ง
|
||||
- ต้องมีแผนการกู้คืนระบบ (Disaster Recovery Plan) ในกรณีที่ Server หลัก (QNAP) ใช้งานไม่ได้
|
||||
- ขั้นตอนการกู้คืน:
|
||||
- **Database Restoration Procedure:**
|
||||
- สร้างจาก full backup ล่าสุด
|
||||
- Apply transaction logs ถึง point-in-time ที่ต้องการ
|
||||
- Verify data integrity post-restoration
|
||||
- **File Storage Restoration Procedure:**
|
||||
- Restore จาก QNAP snapshot หรือ backup
|
||||
- Verify file integrity และ permissions
|
||||
- **Application Redeployment Procedure:**
|
||||
- Deploy จาก version ล่าสุดที่รู้ว่าทำงานได้
|
||||
- Verify application health
|
||||
- **Data Integrity Verification Post-Recovery:**
|
||||
- Run data consistency checks
|
||||
- Verify critical business data
|
||||
- **Recovery Time Objective (RTO):** < 4 ชั่วโมง
|
||||
- **Recovery Point Objective (RPO):** < 1 ชั่วโมง
|
||||
|
||||
## 4.15. กลยุทธ์การแจ้งเตือน (Notification Strategy - ปรับปรุง)
|
||||
|
||||
- ระบบจะส่งการแจ้งเตือน (ผ่าน Email หรือ Line [cite: 2.7]) เมื่อมีการกระทำที่สำคัญ** ดังนี้:
|
||||
1. เมื่อมีเอกสารใหม่ (Correspondence, RFA) ถูกส่งมาถึงองค์กรณ์ของเรา
|
||||
2. เมื่อมีใบเวียน (Circulation) ใหม่ มอบหมายงานมาที่เรา
|
||||
3. (ทางเลือก) เมื่อเอกสารที่เราส่งไป ถูกดำเนินการ (เช่น อนุมัติ/ปฏิเสธ)
|
||||
4. (ทางเลือก) เมื่อใกล้ถึงวันครบกำหนด (Deadline) [cite: 3.2.5, 3.6.6, 3.7.5]
|
||||
- Grouping/Digest
|
||||
- กรณีมีการแจ้งเตือนประเภทเดียวกันจำนวนมากในช่วงเวลาสั้นๆ (เช่น Approve เอกสาร 10 ฉบับรวด) ระบบต้อง **รวม (Batch)** เป็น 1 Email/Line Notification เพื่อไม่ให้รบกวนผู้ใช้ (Spamming)
|
||||
- Notification Delivery Guarantees
|
||||
- **At-least-once delivery:** สำหรับ important notifications
|
||||
- **Retry mechanism:** ด้วย exponential backoff (max 3 reties)
|
||||
- **Dead letter queue:** สำหรับ notifications ที่ส่งไม่สำเร็จหลังจาก retries
|
||||
- **Delivery status tracking:** ต้องบันทึกสถานะการส่ง notifications
|
||||
- **Fallback channels:** ถ้า Email ล้มเหลว ให้ส่งผ่าน SYSTEM notification
|
||||
- **Notification preferences:** ผู้ใช้ต้องสามารถกำหนด channel preferences ได้
|
||||
|
||||
## 4.16. Maintenance Mode
|
||||
|
||||
- ระบบต้องมีกลไก **Maintenance Mode** ที่ Admin สามารถเปิดใช้งานได้
|
||||
- เมื่อเปิด: ผู้ใช้ทั่วไปจะเห็นหน้า "ปิดปรับปรุง" และไม่สามารถเรียก API ได้ (ยกเว้น Admin)
|
||||
- ใช้สำหรับช่วง Deploy Version ใหม่ หรือ Database Migration
|
||||
|
||||
## 4.17. Monitoring และ Observability
|
||||
|
||||
- Application Monitoring
|
||||
- **Health checks:** /health endpoint สำหรับ load balancer
|
||||
- **Metrics collection:** Response times, error rates, throughput
|
||||
- **Distributed tracing:** สำหรับ request tracing across services
|
||||
- **Log aggregation:** Structured logging ด้วย JSON format
|
||||
- **Alerting:** สำหรับ critical errors และ performance degradation
|
||||
- Business Metrics
|
||||
- จำนวน documents created ต่อวัน
|
||||
- Workflow completion rates
|
||||
- User activity metrics
|
||||
- System utilization rates
|
||||
- Search query performance
|
||||
- Security Monitoring
|
||||
- Failed login attempts
|
||||
- Rate limiting triggers
|
||||
- Virus scan results
|
||||
- File download activities
|
||||
- Permission changes
|
||||
|
||||
## 4.18. JSON Processing & Validation
|
||||
|
||||
- JSON Schema Management
|
||||
- ต้องมี centralized JSON schema registry
|
||||
- ต้องรองรับ schema versioning และ migration
|
||||
- ต้องมี schema validation during runtime
|
||||
- Performance Optimization
|
||||
- **Caching:** Cache parsed JSON structures
|
||||
- **Compression:** ใช้ compression สำหรับ JSON ขนาดใหญ่
|
||||
- **Indexing:** Support JSON path indexing สำหรับ query
|
||||
- Error Handling
|
||||
- ต้องมี graceful degradation เมื่อ JSON validation ล้มเหลว
|
||||
- ต้องมี default fallback values
|
||||
- ต้องบันทึก error logs สำหรับ validation failures
|
||||
|
||||
|
||||
# 5. UI/UX Guidelines
|
||||
# 👥 Section 5: UI/UX Requirements (ข้อกำหนดด้านผู้ใช้งาน)
|
||||
---
|
||||
title: 'UI/UX Requirements'
|
||||
version: 1.5.0
|
||||
status: first-draft
|
||||
owner: Nattanin Peancharoen
|
||||
last_updated: 2025-11-30
|
||||
related:
|
||||
|
||||
- specs/02-architecture/data-model.md#correspondence
|
||||
- specs/03-implementation/backend-guidelines.md#correspondencemodule
|
||||
---
|
||||
|
||||
## 5.1. Layout หลัก
|
||||
|
||||
- หน้าเว็บใช้รูปแบบ App Shell ที่ประกอบด้วย
|
||||
- Navbar (ส่วนบน): แสดงชื่อระบบ, เมนูผู้ใช้ (Profile), เมนูสำหรับ Document Control/เมนูสำหรับ Admin/Superadmin (จัดการผู้ใช้, จัดการสิทธิ์, และอื่นๆ), และปุ่ม Login/Logout
|
||||
- Sidebar (ด้านข้าง): เป็นเมนูหลักสำหรับเข้าถึงส่วนที่เกี่ยวข้องกับเอกสารทั้งหมด เช่น Dashboard, Correspondences, RFA, Drawings
|
||||
- Main Content Area: พื้นที่สำหรับแสดงเนื้อหาหลักของหน้าที่เลือก
|
||||
|
||||
## 5.2. หน้า Landing Page
|
||||
|
||||
- เป็นหน้าแรกที่แสดงข้อมูลบางส่วนของโครงการสำหรับผู้ใช้ที่ยังไม่ได้ล็อกอิน
|
||||
|
||||
## 5.3. หน้า Dashboard
|
||||
|
||||
- เป็นหน้าแรกหลังจากล็อกอิน ประกอบด้วย
|
||||
- การ์ดสรุปภาพรวม (KPI Cards): แสดงข้อมูลสรุปที่สำคัญขององค์กร เช่น จำนวนเอกสาร, งานที่เกินกำหนด
|
||||
- ตาราง "งานของฉัน" (My Tasks Table): แสดงรายการงานทั้งหมดจาก Circulation ที่ผู้ใช้ต้องดำเนินการ
|
||||
- Security Metrics: แสดงจำนวน files scanned, security incidents, failed login attempts
|
||||
|
||||
## 5.4. การติดตามสถานะ
|
||||
|
||||
- องค์กรสามารถติดตามสถานะเอกสารทั้งของตนเอง (Originator) และสถานะเอกสารที่ส่งมาถึงตนเอง (Recipient)
|
||||
|
||||
## 5.5. การจัดการข้อมูลส่วนตัว (Profile Page)
|
||||
|
||||
- ผู้ใช้สามารถจัดการข้อมูลส่วนตัวและเปลี่ยนรหัสผ่านของตนเองได้
|
||||
|
||||
## 5.6. การจัดการเอกสารทางเทคนิค (RFA)
|
||||
|
||||
- ผู้ใช้สามารถดู RFA ในรูปแบบ Workflow Diagram ทั้งหมดได้ในหน้าเดียว
|
||||
- Interactive History (เพิ่ม): ในแผนภาพ Workflow ผู้ใช้ต้องสามารถ คลิกที่ Node หรือ Step เก่าที่ผ่านมาแล้ว เพื่อดู Audit Log ย่อยของ Step นั้นได้ทันที (เช่น ใครเป็นคนกด Approve, เวลาไหน, มี Comment อะไร) โดยไม่ต้องสลับไปดูใน Tab History แยกต่างหาก
|
||||
- ขั้นตอนที่ยังไม่ถึงหรือผ่านไปแล้วจะเป็นรูปแบบ disabled
|
||||
- สามารถดำเนินการได้เฉพาะในขั้นตอนที่ได้รับมอบหมายงาน (active)
|
||||
- สิทธิ์ Document Control ขึ้นไป สามารถกด "Force Proceed" ไปยังขั้นตอนต่อไปได้ทุกขั้นตอน, หรือ "Revert" กลับขั้นตอนก่อนหน้าได้
|
||||
|
||||
## 5.7. การจัดการใบเวียนเอกสาร (Circulation)
|
||||
|
||||
- ผู้ใช้สามารถดู Circulation ในรูปแบบ Workflow ทั้งหมดได้ในหน้าเดียว,ขั้นตอนที่ยังไม่ถึงหรือผ่านไปแล้วจะเป็นรูปแบบ disabled, สามารถดำเนินการได้เฉพาะในขั้นตอนที่ได้รับมอบหมายงาน (active) เช่น ตรวจสอบแล้ว เพื่อไปยังขั้นตอนต่อไป, สิทธิ์ Document Control ขึ้นไป สามารถกด ไปยังขั้นตอนต่อไป ได้ทุกขั้นตอน, การย้อนกลับ ไปขั้นตอนก่อนหน้า สามารถทำได้โดย สิทธิ์ Document Control ขึ้นไป
|
||||
|
||||
## 5.8. การจัดการเอกสารนำส่ง (Transmittals)
|
||||
|
||||
- ผู้ใช้สามารถดู Transmittals ในรูปแบบรายการทั้งหมดได้ในหน้าเดียว
|
||||
|
||||
## 5.9. ข้อกำหนด UI/UX การแนบไฟล์ (File Attachment UX)
|
||||
|
||||
- ระบบต้องรองรับการอัปโหลดไฟล์หลายไฟล์พร้อมกัน (Multi-file upload) เช่น การลากและวาง (Drag-and-Drop)
|
||||
- ในหน้าอัปโหลด (เช่น สร้าง RFA หรือ Correspondence) ผู้ใช้ต้องสามารถกำหนดได้ว่าไฟล์ใดเป็น "เอกสารหลัก" (Main Document เช่น PDF) และไฟล์ใดเป็น "เอกสารแนบประกอบ" (Supporting Attachments เช่น .dwg, .docx, .zip)
|
||||
- **Security Feedback:** แสดง security warnings สำหรับ file types ที่เสี่ยงหรือ files ที่ fail virus scan
|
||||
- **File Type Indicators:** แสดง file type icons และ security status
|
||||
|
||||
## 5.10 Form & Interaction
|
||||
|
||||
- **Dynamic Form Generator:** ใช้ Component กลางที่รับ JSON Schema แล้ว Render Form ออกมาอัตโนมัติ เพื่อลดความซ้ำซ้อนของโค้ดหน้าบ้าน และรองรับเอกสารประเภทใหม่ๆ ได้ทันที
|
||||
- **Optimistic Updates:** การเปลี่ยนสถานะ (เช่น กด Approve, กด Read) ให้ UI เปลี่ยนสถานะทันทีให้ผู้ใช้เห็นก่อนรอ API Response (Rollback ถ้า Failed)
|
||||
|
||||
## 5.11 Mobile Responsiveness
|
||||
|
||||
- **Table Visualization:** บนหน้าจอมือถือ ตารางข้อมูลที่มีหลาย Column (เช่น Correspondence List) ต้องเปลี่ยนการแสดงผลเป็นแบบ **Card View** อัตโนมัติ
|
||||
- **Navigation:** Sidebar ต้องเป็นแบบ Collapsible Drawer
|
||||
|
||||
## 5.12 Resilience & Offline Support
|
||||
|
||||
- **Auto-Save Draft:** ระบบต้องบันทึกข้อมูลฟอร์มที่กำลังกรอกลง **LocalStorage** อัตโนมัติ เพื่อป้องกันข้อมูลหายกรณีเน็ตหลุดหรือปิด Browser โดยไม่ได้ตั้งใจ
|
||||
- **State Management:** ใช้ State Management ที่เหมาะสมและไม่ซับซ้อนเกินไป โดยเน้นการใช้ React Query สำหรับ Server State และ React Hook Form สำหรับ Form State
|
||||
- **Graceful Degradation:** หาก Service รอง (เช่น Search, Notification) ล่ม ระบบหลัก (CRUD) ต้องยังทำงานต่อได้
|
||||
|
||||
## 5.13. Secure In-App PDF Viewer (ใหม่)
|
||||
|
||||
- 5.13.1 Viewer Capabilities: ระบบต้องมี PDF Viewer ภายในแอปพลิเคชันที่สามารถเปิดดูไฟล์เอกสารหลัก (PDF) ได้ทันทีโดยไม่ต้องดาวน์โหลดลงเครื่อง เพื่อความสะดวกในการตรวจทาน (Review/Approve)
|
||||
- 5.13.2 Security: การแสดงผลไฟล์ต้อง ห้าม (Disable) การทำ Browser Cache สำหรับไฟล์ Sensitive เพื่อป้องกันการกู้คืนไฟล์จากเครื่อง Client ภายหลัง
|
||||
- 5.13.3 Performance: ต้องรองรับการส่งข้อมูลแบบ Streaming (Range Requests) เพื่อให้เปิดดูไฟล์ขนาดใหญ่ (เช่น แบบแปลน 50MB+) ได้รวดเร็วโดยไม่ต้องรอโหลดเสร็จทั้งไฟล์
|
||||
|
||||
## 🧪 6. Testing Requirements
|
||||
## 6.1 Unit Testing
|
||||
|
||||
- ต้องมี unit tests สำหรับ business logic ทั้งหมด
|
||||
- Code coverage อย่างน้อย 70% สำหรับ backend services
|
||||
- Business Logic: 80%+
|
||||
- Controllers: 70%+
|
||||
- Utilities: 90%+
|
||||
- ต้องทดสอบ RBAC permission logic ทุกระดับ
|
||||
|
||||
## 6.2 Integration Testing
|
||||
|
||||
- ทดสอบการทำงานร่วมกันของ modules
|
||||
- ทดสอบ database migrations และ data integrity
|
||||
- ทดสอบ API endpoints ด้วย realistic data
|
||||
|
||||
## 6.3 End-to-End Testing
|
||||
|
||||
- ทดสอบ complete user workflows
|
||||
- ทดสอบ document lifecycle จาก creation ถึง archival
|
||||
- ทดสอบ cross-module integrations
|
||||
|
||||
## 6.4 Security Testing
|
||||
|
||||
- Penetration Testing: ทดสอบ OWASP Top 10 vulnerabilities
|
||||
- Security Audit: Review code สำหรับ security flaws
|
||||
- Virus Scanning Test: ทดสอบ file upload security
|
||||
- Rate Limiting Test: ทดสอบ rate limiting functionality
|
||||
|
||||
## 6.5 Performance Testing
|
||||
|
||||
- **Load Testing:** ทดสอบด้วย realistic workloads
|
||||
- **Stress Testing:** หา breaking points ของระบบ
|
||||
- **Endurance Testing:** ทดสอบการทำงานต่อเนื่องเป็นเวลานาน
|
||||
|
||||
## 6.6 Disaster Recovery Testing
|
||||
|
||||
- ทดสอบ backup และ restoration procedures
|
||||
- ทดสอบ failover mechanisms
|
||||
- ทดสอบ data integrity หลังการ recovery
|
||||
|
||||
## 6.7 Specific Scenario Testing (เพิ่ม)
|
||||
|
||||
- **Race Condition Test:** ทดสอบยิง Request ขอเลขที่เอกสารพร้อมกัน 100 Request
|
||||
- **Transaction Test:** ทดสอบปิดเน็ตระหว่าง Upload ไฟล์ (ตรวจสอบว่าไม่มี Orphan File หรือ Broken Link)
|
||||
- **Permission Test:** ทดสอบ CASL Integration ทั้งฝั่ง Backend และ Frontend ให้ตรงกัน
|
||||
|
||||
---
|
||||
*Version History*
|
||||
- **v1.5.1** – 2025‑12‑04 – Consolidated requirement specifications into single document.
|
||||
1448
docs/1_FullStackJS_V1_5_1.md
Normal file
1448
docs/1_FullStackJS_V1_5_1.md
Normal file
File diff suppressed because it is too large
Load Diff
210
docs/2_Backend_Plan_V1_5_1.md
Normal file
210
docs/2_Backend_Plan_V1_5_1.md
Normal 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**
|
||||
182
docs/3_Frontend_Plan_V1_5_1.md
Normal file
182
docs/3_Frontend_Plan_V1_5_1.md
Normal 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**
|
||||
1866
docs/4_Data_Dictionary_V1_5_1.md
Normal file
1866
docs/4_Data_Dictionary_V1_5_1.md
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user