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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export class CorrespondenceService {
|
||||
originatorId: userOrgId,
|
||||
typeId: createDto.typeId,
|
||||
disciplineId: createDto.disciplineId, // ส่ง Discipline (ถ้ามี)
|
||||
subTypeId: createDto.subTypeId, // ส่ง SubType (ถ้ามี)
|
||||
subTypeId: createDto.subTypeId, // ส่ง SubType (ถ้ามี)
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
TYPE_CODE: type.typeCode,
|
||||
@@ -165,6 +165,27 @@ export class CorrespondenceService {
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// [NEW V1.5.1] Start Workflow Instance (After Commit)
|
||||
try {
|
||||
const workflowCode = `CORRESPONDENCE_${type.typeCode}`;
|
||||
await this.workflowEngine.createInstance(
|
||||
workflowCode,
|
||||
'correspondence',
|
||||
savedCorr.id.toString(),
|
||||
{
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
disciplineId: createDto.disciplineId,
|
||||
initiatorId: user.user_id,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Workflow not started for ${docNumber} (Code: CORRESPONDENCE_${type.typeCode}): ${(error as Error).message}`
|
||||
);
|
||||
// Non-blocking: Document is created, but workflow might not be active.
|
||||
}
|
||||
|
||||
this.searchService.indexDocument({
|
||||
id: savedCorr.id,
|
||||
type: 'correspondence',
|
||||
@@ -183,7 +204,7 @@ export class CorrespondenceService {
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Failed to create correspondence: ${(err as Error).message}`,
|
||||
`Failed to create correspondence: ${(err as Error).message}`
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -218,7 +239,7 @@ export class CorrespondenceService {
|
||||
if (search) {
|
||||
query.andWhere(
|
||||
'(corr.correspondenceNumber LIKE :search OR rev.title LIKE :search)',
|
||||
{ search: `%${search}%` },
|
||||
{ search: `%${search}%` }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -268,7 +289,7 @@ export class CorrespondenceService {
|
||||
|
||||
if (!template || !template.steps?.length) {
|
||||
throw new BadRequestException(
|
||||
'Invalid routing template or no steps defined',
|
||||
'Invalid routing template or no steps defined'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -288,7 +309,7 @@ export class CorrespondenceService {
|
||||
stepPurpose: firstStep.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
processedByUserId: user.user_id,
|
||||
processedAt: new Date(),
|
||||
@@ -308,7 +329,7 @@ export class CorrespondenceService {
|
||||
async processAction(
|
||||
correspondenceId: number,
|
||||
dto: WorkflowActionDto,
|
||||
user: User,
|
||||
user: User
|
||||
) {
|
||||
const correspondence = await this.correspondenceRepo.findOne({
|
||||
where: { id: correspondenceId },
|
||||
@@ -333,19 +354,19 @@ export class CorrespondenceService {
|
||||
|
||||
if (!currentRouting) {
|
||||
throw new BadRequestException(
|
||||
'No active workflow step found for this document',
|
||||
'No active workflow step found for this document'
|
||||
);
|
||||
}
|
||||
|
||||
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'You are not authorized to process this step',
|
||||
'You are not authorized to process this step'
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentRouting.templateId) {
|
||||
throw new InternalServerErrorException(
|
||||
'Routing record missing templateId',
|
||||
'Routing record missing templateId'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -365,7 +386,7 @@ export class CorrespondenceService {
|
||||
currentSeq,
|
||||
totalSteps,
|
||||
dto.action,
|
||||
dto.returnToSequence,
|
||||
dto.returnToSequence
|
||||
);
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
@@ -383,12 +404,12 @@ export class CorrespondenceService {
|
||||
|
||||
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
|
||||
const nextStepConfig = template.steps.find(
|
||||
(s) => s.sequence === result.nextStepSequence,
|
||||
(s) => s.sequence === result.nextStepSequence
|
||||
);
|
||||
|
||||
if (!nextStepConfig) {
|
||||
this.logger.warn(
|
||||
`Next step ${result.nextStepSequence} not found in template`,
|
||||
`Next step ${result.nextStepSequence} not found in template`
|
||||
);
|
||||
} else {
|
||||
const nextRouting = queryRunner.manager.create(
|
||||
@@ -403,9 +424,9 @@ export class CorrespondenceService {
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() +
|
||||
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
},
|
||||
}
|
||||
);
|
||||
await queryRunner.manager.save(nextRouting);
|
||||
}
|
||||
@@ -478,4 +499,4 @@ export class CorrespondenceService {
|
||||
|
||||
return { outgoing, incoming };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ export class Correspondence {
|
||||
@Column({ name: 'correspondence_type_id' })
|
||||
correspondenceTypeId!: number;
|
||||
|
||||
@Column({ name: 'discipline_id', nullable: true })
|
||||
disciplineId?: number;
|
||||
|
||||
@Column({ name: 'project_id' })
|
||||
projectId!: number;
|
||||
|
||||
@@ -64,10 +67,15 @@ export class Correspondence {
|
||||
@JoinColumn({ name: 'created_by' })
|
||||
creator?: User;
|
||||
|
||||
// [New V1.5.1]
|
||||
@ManyToOne('Discipline')
|
||||
@JoinColumn({ name: 'discipline_id' })
|
||||
discipline?: any; // Use 'any' or import Discipline entity if available to avoid circular dependency issues if not careful, but better to import.
|
||||
|
||||
// One Correspondence has Many Revisions
|
||||
@OneToMany(
|
||||
() => CorrespondenceRevision,
|
||||
(revision) => revision.correspondence,
|
||||
(revision) => revision.correspondence
|
||||
)
|
||||
revisions?: CorrespondenceRevision[];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
ConflictException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
@@ -38,7 +36,7 @@ export class ShopDrawingService {
|
||||
@InjectRepository(Attachment)
|
||||
private attachmentRepo: Repository<Attachment>,
|
||||
private fileStorageService: FileStorageService,
|
||||
private dataSource: DataSource,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -51,7 +49,7 @@ export class ShopDrawingService {
|
||||
});
|
||||
if (exists) {
|
||||
throw new ConflictException(
|
||||
`Drawing number "${createDto.drawingNumber}" already exists.`,
|
||||
`Drawing number "${createDto.drawingNumber}" already exists.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +101,7 @@ export class ShopDrawingService {
|
||||
// 5. Commit Files
|
||||
if (createDto.attachmentIds?.length) {
|
||||
await this.fileStorageService.commit(
|
||||
createDto.attachmentIds.map(String),
|
||||
createDto.attachmentIds.map(String)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,7 +115,7 @@ export class ShopDrawingService {
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Failed to create shop drawing: ${(err as Error).message}`,
|
||||
`Failed to create shop drawing: ${(err as Error).message}`
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
@@ -130,7 +128,7 @@ export class ShopDrawingService {
|
||||
*/
|
||||
async createRevision(
|
||||
shopDrawingId: number,
|
||||
createDto: CreateShopDrawingRevisionDto,
|
||||
createDto: CreateShopDrawingRevisionDto
|
||||
) {
|
||||
const shopDrawing = await this.shopDrawingRepo.findOneBy({
|
||||
id: shopDrawingId,
|
||||
@@ -144,7 +142,7 @@ export class ShopDrawingService {
|
||||
});
|
||||
if (exists) {
|
||||
throw new ConflictException(
|
||||
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`,
|
||||
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,7 +186,7 @@ export class ShopDrawingService {
|
||||
|
||||
if (createDto.attachmentIds?.length) {
|
||||
await this.fileStorageService.commit(
|
||||
createDto.attachmentIds.map(String),
|
||||
createDto.attachmentIds.map(String)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,7 +235,7 @@ export class ShopDrawingService {
|
||||
qb.where('sd.drawingNumber LIKE :search', {
|
||||
search: `%${search}%`,
|
||||
}).orWhere('sd.title LIKE :search', { search: `%${search}%` });
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
70
backend/src/modules/project/contract.controller.ts
Normal file
70
backend/src/modules/project/contract.controller.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiBearerAuth,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { ContractService } from './contract.service.js';
|
||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
@ApiTags('Contracts')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('contracts')
|
||||
export class ContractController {
|
||||
constructor(private readonly contractService: ContractService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Create Contract' })
|
||||
create(@Body() dto: CreateContractDto) {
|
||||
return this.contractService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'Get All Contracts (Optional: filter by projectId)',
|
||||
})
|
||||
@ApiQuery({ name: 'projectId', required: false, type: Number })
|
||||
findAll(@Query('projectId') projectId?: number) {
|
||||
return this.contractService.findAll(projectId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Contract by ID' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.contractService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Update Contract' })
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: UpdateContractDto
|
||||
) {
|
||||
return this.contractService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Delete Contract' })
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.contractService.remove(id);
|
||||
}
|
||||
}
|
||||
65
backend/src/modules/project/contract.service.ts
Normal file
65
backend/src/modules/project/contract.service.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Contract } from './entities/contract.entity.js';
|
||||
import { CreateContractDto } from './dto/create-contract.dto.js';
|
||||
import { UpdateContractDto } from './dto/update-contract.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class ContractService {
|
||||
constructor(
|
||||
@InjectRepository(Contract)
|
||||
private readonly contractRepo: Repository<Contract>
|
||||
) {}
|
||||
|
||||
async create(dto: CreateContractDto) {
|
||||
const existing = await this.contractRepo.findOne({
|
||||
where: { contractCode: dto.contractCode },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Contract Code "${dto.contractCode}" already exists`
|
||||
);
|
||||
}
|
||||
const contract = this.contractRepo.create(dto);
|
||||
return this.contractRepo.save(contract);
|
||||
}
|
||||
|
||||
async findAll(projectId?: number) {
|
||||
const query = this.contractRepo
|
||||
.createQueryBuilder('c')
|
||||
.leftJoinAndSelect('c.project', 'p')
|
||||
.orderBy('c.contractCode', 'ASC');
|
||||
|
||||
if (projectId) {
|
||||
query.where('c.projectId = :projectId', { projectId });
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const contract = await this.contractRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['project'],
|
||||
});
|
||||
if (!contract) throw new NotFoundException(`Contract ID ${id} not found`);
|
||||
return contract;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateContractDto) {
|
||||
const contract = await this.findOne(id);
|
||||
Object.assign(contract, dto);
|
||||
return this.contractRepo.save(contract);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const contract = await this.findOne(id);
|
||||
// Schema doesn't have deleted_at for Contract either.
|
||||
return this.contractRepo.remove(contract);
|
||||
}
|
||||
}
|
||||
49
backend/src/modules/project/dto/create-contract.dto.ts
Normal file
49
backend/src/modules/project/dto/create-contract.dto.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
Length,
|
||||
IsInt,
|
||||
IsDateString,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateContractDto {
|
||||
@ApiProperty({ example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number;
|
||||
|
||||
@ApiProperty({ example: 'C-001' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 50)
|
||||
contractCode!: string;
|
||||
|
||||
@ApiProperty({ example: 'Main Construction Contract' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 255)
|
||||
contractName!: string;
|
||||
|
||||
@ApiProperty({ example: 'Description of the contract', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: '2024-01-01', required: false })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startDate?: string; // Receive as string, TypeORM handles date
|
||||
|
||||
@ApiProperty({ example: '2025-12-31', required: false })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endDate?: string;
|
||||
|
||||
@ApiProperty({ example: true, required: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
27
backend/src/modules/project/dto/create-organization.dto.ts
Normal file
27
backend/src/modules/project/dto/create-organization.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
Length,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateOrganizationDto {
|
||||
@ApiProperty({ example: 'ITD' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 20)
|
||||
organizationCode!: string;
|
||||
|
||||
@ApiProperty({ example: 'Italian-Thai Development' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Length(1, 255)
|
||||
organizationName!: string;
|
||||
|
||||
@ApiProperty({ example: true, required: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
4
backend/src/modules/project/dto/update-contract.dto.ts
Normal file
4
backend/src/modules/project/dto/update-contract.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateContractDto } from './create-contract.dto.js';
|
||||
|
||||
export class UpdateContractDto extends PartialType(CreateContractDto) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateOrganizationDto } from './create-organization.dto.js';
|
||||
|
||||
export class UpdateOrganizationDto extends PartialType(CreateOrganizationDto) {}
|
||||
61
backend/src/modules/project/organization.controller.ts
Normal file
61
backend/src/modules/project/organization.controller.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { OrganizationService } from './organization.service.js';
|
||||
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||
|
||||
@ApiTags('Organizations')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('organizations')
|
||||
export class OrganizationController {
|
||||
constructor(private readonly orgService: OrganizationService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Create Organization' })
|
||||
create(@Body() dto: CreateOrganizationDto) {
|
||||
return this.orgService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get All Organizations' })
|
||||
findAll() {
|
||||
return this.orgService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Organization by ID' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.orgService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Update Organization' })
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: UpdateOrganizationDto
|
||||
) {
|
||||
return this.orgService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('master_data.manage')
|
||||
@ApiOperation({ summary: 'Delete Organization' })
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.orgService.remove(id);
|
||||
}
|
||||
}
|
||||
57
backend/src/modules/project/organization.service.ts
Normal file
57
backend/src/modules/project/organization.service.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
import { CreateOrganizationDto } from './dto/create-organization.dto.js';
|
||||
import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationService {
|
||||
constructor(
|
||||
@InjectRepository(Organization)
|
||||
private readonly orgRepo: Repository<Organization>
|
||||
) {}
|
||||
|
||||
async create(dto: CreateOrganizationDto) {
|
||||
const existing = await this.orgRepo.findOne({
|
||||
where: { organizationCode: dto.organizationCode },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
`Organization Code "${dto.organizationCode}" already exists`
|
||||
);
|
||||
}
|
||||
const org = this.orgRepo.create(dto);
|
||||
return this.orgRepo.save(org);
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.orgRepo.find({
|
||||
order: { organizationCode: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const org = await this.orgRepo.findOne({ where: { id } });
|
||||
if (!org) throw new NotFoundException(`Organization ID ${id} not found`);
|
||||
return org;
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateOrganizationDto) {
|
||||
const org = await this.findOne(id);
|
||||
Object.assign(org, dto);
|
||||
return this.orgRepo.save(org);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const org = await this.findOne(id);
|
||||
// Hard delete or Soft delete? Schema doesn't have deleted_at for Organization, but let's check.
|
||||
// Schema says: created_at, updated_at. No deleted_at.
|
||||
// So hard delete.
|
||||
return this.orgRepo.remove(org);
|
||||
}
|
||||
}
|
||||
@@ -2,26 +2,32 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ProjectService } from './project.service.js';
|
||||
import { ProjectController } from './project.controller.js';
|
||||
import { OrganizationService } from './organization.service.js';
|
||||
import { OrganizationController } from './organization.controller.js';
|
||||
import { ContractService } from './contract.service.js';
|
||||
import { ContractController } from './contract.controller.js';
|
||||
|
||||
import { Project } from './entities/project.entity.js';
|
||||
import { Organization } from './entities/organization.entity.js';
|
||||
import { Contract } from './entities/contract.entity.js';
|
||||
import { ProjectOrganization } from './entities/project-organization.entity.js'; // เพิ่ม
|
||||
import { ContractOrganization } from './entities/contract-organization.entity.js'; // เพิ่ม
|
||||
import { ProjectOrganization } from './entities/project-organization.entity.js';
|
||||
import { ContractOrganization } from './entities/contract-organization.entity.js';
|
||||
// Modules
|
||||
import { UserModule } from '../user/user.module'; // ✅ 1. Import UserModule
|
||||
import { UserModule } from '../user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Project,
|
||||
Organization,
|
||||
Contract,
|
||||
ProjectOrganization, // ลงทะเบียน
|
||||
ContractOrganization, // ลงทะเบียน
|
||||
ProjectOrganization,
|
||||
ContractOrganization,
|
||||
]),
|
||||
UserModule, // ✅ 2. เพิ่ม UserModule เข้าไปใน imports
|
||||
UserModule,
|
||||
],
|
||||
controllers: [ProjectController],
|
||||
providers: [ProjectService],
|
||||
exports: [ProjectService], // Export เผื่อ Module อื่นใช้
|
||||
controllers: [ProjectController, OrganizationController, ContractController],
|
||||
providers: [ProjectService, OrganizationService, ContractService],
|
||||
exports: [ProjectService, OrganizationService, ContractService],
|
||||
})
|
||||
export class ProjectModule {}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { WorkflowActionDto } from '../correspondence/dto/workflow-action.dto';
|
||||
import { CreateRfaDto } from './dto/create-rfa.dto';
|
||||
|
||||
// Interfaces & Enums
|
||||
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface';
|
||||
|
||||
// Services
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
@@ -68,7 +69,7 @@ export class RfaService {
|
||||
private workflowEngine: WorkflowEngineService,
|
||||
private notificationService: NotificationService,
|
||||
private dataSource: DataSource,
|
||||
private searchService: SearchService,
|
||||
private searchService: SearchService
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateRfaDto, user: User) {
|
||||
@@ -82,7 +83,7 @@ export class RfaService {
|
||||
});
|
||||
if (!statusDraft) {
|
||||
throw new InternalServerErrorException(
|
||||
'Status DFT (Draft) not found in Master Data',
|
||||
'Status DFT (Draft) not found in Master Data'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,24 +120,20 @@ export class RfaService {
|
||||
// 1. Create Correspondence Record
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber,
|
||||
correspondenceTypeId: createDto.rfaTypeId, // Assuming RFA Type maps directly or via logic
|
||||
// Note: ถ้า CorrespondenceType แยก ID กับ RFA Type ต้อง Map ให้ถูก
|
||||
// ในที่นี้สมมติว่าใช้ ID เดียวกัน หรือ RFA Type เป็น SubType ของ Correspondence
|
||||
correspondenceTypeId: createDto.rfaTypeId,
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
isInternalCommunication: false,
|
||||
isInternal: false,
|
||||
createdBy: user.user_id,
|
||||
// ✅ Add disciplineId column if correspondence table supports it (as per Data Dictionary Update)
|
||||
// disciplineId: createDto.disciplineId
|
||||
disciplineId: createDto.disciplineId, // ✅ Add disciplineId
|
||||
});
|
||||
const savedCorr = await queryRunner.manager.save(correspondence);
|
||||
|
||||
// 2. Create RFA Master Record
|
||||
// 2. Create Rfa Master Record
|
||||
const rfa = queryRunner.manager.create(Rfa, {
|
||||
rfaTypeId: createDto.rfaTypeId,
|
||||
createdBy: user.user_id,
|
||||
// ✅ ถ้า Entity Rfa มี disciplineId ให้ใส่ตรงนี้ด้วย
|
||||
// disciplineId: createDto.disciplineId
|
||||
disciplineId: createDto.disciplineId, // ✅ Add disciplineId
|
||||
});
|
||||
const savedRfa = await queryRunner.manager.save(rfa);
|
||||
|
||||
@@ -154,8 +151,8 @@ export class RfaService {
|
||||
? new Date(createDto.documentDate)
|
||||
: new Date(),
|
||||
createdBy: user.user_id,
|
||||
details: createDto.details, // ✅ Save JSON Details
|
||||
schemaVersion: 1, // ✅ Default Schema Version
|
||||
details: createDto.details,
|
||||
schemaVersion: 1,
|
||||
});
|
||||
const savedRevision = await queryRunner.manager.save(rfaRevision);
|
||||
|
||||
@@ -174,27 +171,48 @@ export class RfaService {
|
||||
|
||||
const rfaItems = shopDrawings.map((sd) =>
|
||||
queryRunner.manager.create(RfaItem, {
|
||||
rfaRevisionId: savedCorr.id, // ใช้ ID ของ Correspondence (ตาม Schema ที่ออกแบบไว้) หรือ RFA Revision ID แล้วแต่การ Map Entity
|
||||
// ตาม Entity RfaItem ที่ให้มา: rfaRevisionId map ไปที่ correspondence_id
|
||||
rfaRevisionId: savedCorr.id, // Use Correspondence ID as per schema
|
||||
shopDrawingRevisionId: sd.id,
|
||||
}),
|
||||
})
|
||||
);
|
||||
await queryRunner.manager.save(rfaItems);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// [NEW V1.5.1] Start Unified Workflow Instance
|
||||
try {
|
||||
const workflowCode = `RFA_${rfaType.typeCode}`; // e.g., RFA_GEN
|
||||
await this.workflowEngine.createInstance(
|
||||
workflowCode,
|
||||
'rfa',
|
||||
savedRfa.id.toString(),
|
||||
{
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
disciplineId: createDto.disciplineId,
|
||||
initiatorId: user.user_id,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Workflow not started for ${docNumber}: ${(error as Error).message}`
|
||||
);
|
||||
}
|
||||
|
||||
// Indexing for Search
|
||||
this.searchService.indexDocument({
|
||||
id: savedCorr.id,
|
||||
type: 'rfa',
|
||||
docNumber: docNumber,
|
||||
title: createDto.title,
|
||||
description: createDto.description,
|
||||
status: 'DRAFT',
|
||||
projectId: createDto.projectId,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
this.searchService
|
||||
.indexDocument({
|
||||
id: savedCorr.id,
|
||||
type: 'rfa',
|
||||
docNumber: docNumber,
|
||||
title: createDto.title,
|
||||
description: createDto.description,
|
||||
status: 'DRAFT',
|
||||
projectId: createDto.projectId,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.catch((err) => this.logger.error(`Indexing failed: ${err}`));
|
||||
|
||||
return {
|
||||
...savedRfa,
|
||||
@@ -284,7 +302,7 @@ export class RfaService {
|
||||
stepPurpose: firstStep.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
processedByUserId: user.user_id,
|
||||
processedAt: new Date(),
|
||||
@@ -293,7 +311,7 @@ export class RfaService {
|
||||
|
||||
// Notify
|
||||
const recipientUserId = await this.userService.findDocControlIdByOrg(
|
||||
firstStep.toOrganizationId,
|
||||
firstStep.toOrganizationId
|
||||
);
|
||||
if (recipientUserId) {
|
||||
await this.notificationService.send({
|
||||
@@ -338,7 +356,7 @@ export class RfaService {
|
||||
throw new BadRequestException('No active workflow step found');
|
||||
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
|
||||
throw new ForbiddenException(
|
||||
'You are not authorized to process this step',
|
||||
'You are not authorized to process this step'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -355,7 +373,7 @@ export class RfaService {
|
||||
currentRouting.sequence,
|
||||
template.steps.length,
|
||||
dto.action,
|
||||
dto.returnToSequence,
|
||||
dto.returnToSequence
|
||||
);
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
@@ -364,16 +382,17 @@ export class RfaService {
|
||||
|
||||
try {
|
||||
// Update current routing
|
||||
currentRouting.status = dto.action === 'REJECT' ? 'REJECTED' : 'ACTIONED';
|
||||
currentRouting.status =
|
||||
dto.action === WorkflowAction.REJECT ? 'REJECTED' : 'ACTIONED';
|
||||
currentRouting.processedByUserId = user.user_id;
|
||||
currentRouting.processedAt = new Date();
|
||||
currentRouting.comments = dto.comments;
|
||||
await queryRunner.manager.save(currentRouting);
|
||||
|
||||
// Create next routing if available
|
||||
if (result.nextStepSequence && dto.action !== 'REJECT') {
|
||||
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
|
||||
const nextStep = template.steps.find(
|
||||
(s) => s.sequence === result.nextStepSequence,
|
||||
(s) => s.sequence === result.nextStepSequence
|
||||
);
|
||||
if (nextStep) {
|
||||
const nextRouting = queryRunner.manager.create(
|
||||
@@ -387,18 +406,20 @@ export class RfaService {
|
||||
stepPurpose: nextStep.stepPurpose,
|
||||
status: 'SENT',
|
||||
dueDate: new Date(
|
||||
Date.now() + (nextStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
|
||||
Date.now() + (nextStep.expectedDays || 7) * 24 * 60 * 60 * 1000
|
||||
),
|
||||
},
|
||||
}
|
||||
);
|
||||
await queryRunner.manager.save(nextRouting);
|
||||
}
|
||||
} else if (result.nextStepSequence === null) {
|
||||
// Workflow Ended (Completed or Rejected)
|
||||
// Update RFA Status (Approved/Rejected Code)
|
||||
if (dto.action !== 'REJECT') {
|
||||
if (dto.action !== WorkflowAction.REJECT) {
|
||||
const approveCode = await this.rfaApproveRepo.findOne({
|
||||
where: { approveCode: dto.action === 'APPROVE' ? '1A' : '4X' },
|
||||
where: {
|
||||
approveCode: dto.action === WorkflowAction.APPROVE ? '1A' : '4X',
|
||||
},
|
||||
}); // Logic Map Code อย่างง่าย
|
||||
if (approveCode) {
|
||||
currentRevision.rfaApproveCodeId = approveCode.id;
|
||||
|
||||
@@ -4,11 +4,12 @@ import {
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
ValidateNested,
|
||||
IsEnum,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
// Enum นี้ควรตรงกับใน Entity หรือสร้างไฟล์ enum แยก (ในที่นี้ใส่ไว้ใน DTO เพื่อความสะดวก)
|
||||
export enum TransmittalPurpose {
|
||||
FOR_APPROVAL = 'FOR_APPROVAL',
|
||||
FOR_INFORMATION = 'FOR_INFORMATION',
|
||||
@@ -16,21 +17,57 @@ export enum TransmittalPurpose {
|
||||
OTHER = 'OTHER',
|
||||
}
|
||||
|
||||
export class CreateTransmittalDto {
|
||||
export class TransmittalItemDto {
|
||||
@ApiProperty({
|
||||
description: 'ประเภทรายการ (DRAWING, RFA, CORRESPONDENCE)',
|
||||
example: 'DRAWING',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
itemType!: string;
|
||||
|
||||
@ApiProperty({ description: 'ID ของรายการ', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number; // จำเป็นสำหรับการออกเลขที่เอกสาร (Running Number)
|
||||
|
||||
@IsEnum(TransmittalPurpose)
|
||||
@IsOptional()
|
||||
purpose?: TransmittalPurpose; // วัตถุประสงค์การส่ง
|
||||
itemId!: number;
|
||||
|
||||
@ApiProperty({ description: 'รายละเอียดเพิ่มเติม', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
remarks?: string; // หมายเหตุเพิ่มเติม
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@IsNotEmpty()
|
||||
itemIds!: number[]; // ID ของเอกสาร (Correspondence IDs) ที่จะแนบไปใน Transmittal นี้
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class CreateTransmittalDto {
|
||||
@ApiProperty({ description: 'ID ของโครงการ', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
projectId!: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'เรื่อง',
|
||||
example: 'Transmittal for Shop Drawings',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
subject!: string;
|
||||
|
||||
@ApiProperty({ description: 'ผู้รับ (Organization ID)', example: 2 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
recipientOrganizationId!: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'วัตถุประสงค์',
|
||||
enum: TransmittalPurpose,
|
||||
example: TransmittalPurpose.FOR_APPROVAL,
|
||||
})
|
||||
@IsEnum(TransmittalPurpose)
|
||||
@IsOptional()
|
||||
purpose?: TransmittalPurpose;
|
||||
|
||||
@ApiProperty({ description: 'รายการที่แนบ', type: [TransmittalItemDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TransmittalItemDto)
|
||||
items!: TransmittalItemDto[];
|
||||
}
|
||||
|
||||
@@ -1,36 +1,22 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Entity, Column, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
import { Transmittal } from './transmittal.entity';
|
||||
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
||||
|
||||
@Entity('transmittal_items')
|
||||
export class TransmittalItem {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'transmittal_id' })
|
||||
@PrimaryColumn({ name: 'transmittal_id' })
|
||||
transmittalId!: number;
|
||||
|
||||
@Column({ name: 'item_correspondence_id' })
|
||||
itemCorrespondenceId!: number;
|
||||
@PrimaryColumn({ name: 'item_type', length: 50 })
|
||||
itemType!: string; // DRAWING, RFA, etc.
|
||||
|
||||
@Column({ default: 1 })
|
||||
quantity!: number;
|
||||
@PrimaryColumn({ name: 'item_id' })
|
||||
itemId!: number;
|
||||
|
||||
@Column({ length: 255, nullable: true })
|
||||
remarks?: string;
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Transmittal, (t) => t.items, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'transmittal_id' })
|
||||
transmittal!: Transmittal;
|
||||
|
||||
@ManyToOne(() => Correspondence)
|
||||
@JoinColumn({ name: 'item_correspondence_id' })
|
||||
itemDocument!: Correspondence;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { Correspondence } from '../../correspondence/entities/correspondence.entity';
|
||||
import { TransmittalItem } from './transmittal-item.entity';
|
||||
|
||||
@Entity('transmittals')
|
||||
export class Transmittal {
|
||||
@PrimaryColumn({ name: 'correspondence_id' })
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'correspondence_id', unique: true })
|
||||
correspondenceId!: number;
|
||||
|
||||
@Column({ name: 'transmittal_no', length: 100 })
|
||||
transmittalNo!: string;
|
||||
|
||||
@Column({ length: 500 })
|
||||
subject!: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['FOR_APPROVAL', 'FOR_INFORMATION', 'FOR_REVIEW', 'OTHER'],
|
||||
@@ -24,6 +34,9 @@ export class Transmittal {
|
||||
@Column({ type: 'text', nullable: true })
|
||||
remarks?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt!: Date;
|
||||
|
||||
// Relations
|
||||
@OneToOne(() => Correspondence)
|
||||
@JoinColumn({ name: 'correspondence_id' })
|
||||
|
||||
@@ -4,50 +4,32 @@ import {
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { TransmittalService } from './transmittal.service';
|
||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
|
||||
import { SearchTransmittalDto } from './dto/search-transmittal.dto'; // เดี๋ยวสร้าง DTO นี้เพิ่มให้ครับถ้ายังไม่มี
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('Transmittals')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RbacGuard)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('transmittals')
|
||||
export class TransmittalController {
|
||||
constructor(private readonly transmittalService: TransmittalService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create new Transmittal' })
|
||||
@RequirePermission('transmittal.create') // สิทธิ์ ID 40
|
||||
@Audit('transmittal.create', 'transmittal') // ✅ แปะตรงนี้
|
||||
@ApiOperation({ summary: 'Create a new Transmittal' })
|
||||
create(@Body() createDto: CreateTransmittalDto, @CurrentUser() user: User) {
|
||||
return this.transmittalService.create(createDto, user);
|
||||
}
|
||||
|
||||
// เพิ่ม Endpoint พื้นฐานสำหรับการค้นหา (Optional)
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Search Transmittals' })
|
||||
@RequirePermission('document.view')
|
||||
findAll(@Query() searchDto: SearchTransmittalDto) {
|
||||
// return this.transmittalService.findAll(searchDto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get Transmittal details' })
|
||||
@RequirePermission('document.view')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
// return this.transmittalService.findOne(id);
|
||||
return this.transmittalService.findOne(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,23 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Transmittal } from './entities/transmittal.entity';
|
||||
import { TransmittalItem } from './entities/transmittal-item.entity';
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
||||
import { TransmittalService } from './transmittal.service';
|
||||
import { TransmittalController } from './transmittal.controller';
|
||||
import { DocumentNumberingModule } from '../document-numbering/document-numbering.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { SearchModule } from '../search/search.module'; // ✅ ต้อง Import เพราะ Service ใช้ (ที่เป็นสาเหตุ Error)
|
||||
import { SearchModule } from '../search/search.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Transmittal, TransmittalItem, Correspondence]),
|
||||
TypeOrmModule.forFeature([
|
||||
Transmittal,
|
||||
TransmittalItem,
|
||||
Correspondence,
|
||||
CorrespondenceType,
|
||||
CorrespondenceStatus,
|
||||
]),
|
||||
DocumentNumberingModule,
|
||||
UserModule,
|
||||
SearchModule,
|
||||
|
||||
@@ -1,98 +1,143 @@
|
||||
// File: src/modules/transmittal/transmittal.service.ts
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
InternalServerErrorException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, In } from 'typeorm';
|
||||
|
||||
import { Transmittal } from './entities/transmittal.entity.js';
|
||||
import { TransmittalItem } from './entities/transmittal-item.entity.js';
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity.js';
|
||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto.js';
|
||||
import { User } from '../user/entities/user.entity.js';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service.js';
|
||||
import { SearchService } from '../search/search.service.js';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { Transmittal } from './entities/transmittal.entity';
|
||||
import { TransmittalItem } from './entities/transmittal-item.entity';
|
||||
import { CreateTransmittalDto } from './dto/create-transmittal.dto';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
import { DocumentNumberingService } from '../document-numbering/document-numbering.service';
|
||||
import { Correspondence } from '../correspondence/entities/correspondence.entity';
|
||||
import { CorrespondenceRevision } from '../correspondence/entities/correspondence-revision.entity';
|
||||
import { CorrespondenceType } from '../correspondence/entities/correspondence-type.entity';
|
||||
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TransmittalService {
|
||||
private readonly logger = new Logger(TransmittalService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Transmittal)
|
||||
private transmittalRepo: Repository<Transmittal>,
|
||||
@InjectRepository(TransmittalItem)
|
||||
private transmittalItemRepo: Repository<TransmittalItem>,
|
||||
@InjectRepository(Correspondence)
|
||||
private correspondenceRepo: Repository<Correspondence>,
|
||||
private itemRepo: Repository<TransmittalItem>,
|
||||
@InjectRepository(CorrespondenceType)
|
||||
private typeRepo: Repository<CorrespondenceType>,
|
||||
@InjectRepository(CorrespondenceStatus)
|
||||
private statusRepo: Repository<CorrespondenceStatus>,
|
||||
private numberingService: DocumentNumberingService,
|
||||
private dataSource: DataSource,
|
||||
private searchService: SearchService,
|
||||
private dataSource: DataSource
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateTransmittalDto, user: User) {
|
||||
if (!user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create documents',
|
||||
);
|
||||
}
|
||||
const userOrgId = user.primaryOrganizationId;
|
||||
// 1. Get Transmittal Type (Assuming Code '901' or 'TRN')
|
||||
const type = await this.typeRepo.findOne({
|
||||
where: { typeCode: 'TRN' }, // Adjust code as per Master Data
|
||||
});
|
||||
if (!type) throw new NotFoundException('Transmittal Type (TRN) not found');
|
||||
|
||||
const statusDraft = await this.statusRepo.findOne({
|
||||
where: { statusCode: 'DRAFT' },
|
||||
});
|
||||
if (!statusDraft)
|
||||
throw new InternalServerErrorException('Status DRAFT not found');
|
||||
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
const transmittalTypeId = 3; // TODO: ดึง ID จริงจาก DB หรือ Config
|
||||
const orgCode = 'ORG'; // TODO: Fetch real ORG Code
|
||||
if (!user.primaryOrganizationId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create a transmittal'
|
||||
);
|
||||
}
|
||||
|
||||
// [FIXED] เรียกใช้แบบ Object Context
|
||||
try {
|
||||
// 2. Generate Number
|
||||
const docNumber = await this.numberingService.generateNextNumber({
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
typeId: transmittalTypeId,
|
||||
originatorId: user.primaryOrganizationId,
|
||||
typeId: type.id,
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
TYPE_CODE: 'TR',
|
||||
ORG_CODE: orgCode,
|
||||
TYPE_CODE: type.typeCode,
|
||||
ORG_CODE: 'ORG', // TODO: Fetch real ORG Code
|
||||
},
|
||||
});
|
||||
|
||||
// 3. Create Correspondence (Parent)
|
||||
const correspondence = queryRunner.manager.create(Correspondence, {
|
||||
correspondenceNumber: docNumber,
|
||||
correspondenceTypeId: transmittalTypeId,
|
||||
correspondenceTypeId: type.id,
|
||||
projectId: createDto.projectId,
|
||||
originatorId: userOrgId,
|
||||
originatorId: user.primaryOrganizationId,
|
||||
isInternal: false,
|
||||
createdBy: user.user_id,
|
||||
});
|
||||
const savedCorr = await queryRunner.manager.save(correspondence);
|
||||
|
||||
// 4. Create Revision (Draft)
|
||||
const revision = queryRunner.manager.create(CorrespondenceRevision, {
|
||||
correspondenceId: savedCorr.id,
|
||||
revisionNumber: 0,
|
||||
revisionLabel: '0',
|
||||
isCurrent: true,
|
||||
statusId: statusDraft.id,
|
||||
title: createDto.subject,
|
||||
createdBy: user.user_id,
|
||||
});
|
||||
await queryRunner.manager.save(revision);
|
||||
|
||||
// 5. Create Transmittal
|
||||
const transmittal = queryRunner.manager.create(Transmittal, {
|
||||
correspondenceId: savedCorr.id,
|
||||
purpose: createDto.purpose,
|
||||
remarks: createDto.remarks,
|
||||
transmittalNo: docNumber,
|
||||
subject: createDto.subject,
|
||||
});
|
||||
await queryRunner.manager.save(transmittal);
|
||||
const savedTransmittal = await queryRunner.manager.save(transmittal);
|
||||
|
||||
if (createDto.itemIds && createDto.itemIds.length > 0) {
|
||||
const items = createDto.itemIds.map((itemId) =>
|
||||
// 6. Create Items
|
||||
if (createDto.items && createDto.items.length > 0) {
|
||||
const items = createDto.items.map((item) =>
|
||||
queryRunner.manager.create(TransmittalItem, {
|
||||
transmittalId: savedCorr.id,
|
||||
itemCorrespondenceId: itemId,
|
||||
quantity: 1,
|
||||
}),
|
||||
transmittalId: savedTransmittal.id,
|
||||
itemType: item.itemType,
|
||||
itemId: item.itemId,
|
||||
description: item.description,
|
||||
})
|
||||
);
|
||||
await queryRunner.manager.save(items);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { ...savedCorr, transmittal };
|
||||
|
||||
return {
|
||||
...savedTransmittal,
|
||||
correspondence: savedCorr,
|
||||
};
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error(
|
||||
`Failed to create transmittal: ${(err as Error).message}`
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const transmittal = await this.transmittalRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['correspondence', 'items'],
|
||||
});
|
||||
if (!transmittal)
|
||||
throw new NotFoundException(`Transmittal ID ${id} not found`);
|
||||
return transmittal;
|
||||
}
|
||||
}
|
||||
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -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