251122:1700 Phase 4

This commit is contained in:
admin
2025-11-22 17:21:55 +07:00
parent bf0308e350
commit a3474bff6a
63 changed files with 10062 additions and 109 deletions

View File

@@ -20,6 +20,7 @@
"database": "lcbp3_dev",
"username": "root"
}
]
],
"editor.fontSize": 15
}
}

View File

@@ -1,8 +1,8 @@
# 📝 **Documents Management System Version 1.4.2: Application Requirements Specification**
# 📝 **Documents Management System Version 1.4.3: Application Requirements Specification**
**สถานะ:** FINAL
**วันที่:** 2025-11-19
**อ้างอิงพื้นฐาน:** v1.4.2
**สถานะ:** FINAL-Rev.01
**วันที่:** 2025-11-22
**อ้างอิงพื้นฐาน:** v1.4.3
**Classification:** Internal Technical Documentation
## 📌 **1. วัตถุประสงค์**
@@ -104,7 +104,7 @@
### **2.4 Business Logic & Consistency (ปรับปรุง):**
- **2.4.1 ตรรกะทางธุรกิจที่ซับซ้อนทั้งหมด** (เช่น การเปลี่ยนสถานะ Workflow [cite: 3.5.4, 3.6.5], การบังคับใช้สิทธิ์ [cite: 4.4], การตรวจสอบ Deadline [cite: 3.2.5]) **จะถูกจัดการในฝั่ง Backend (NestJS)** [cite: 2.3] เพื่อให้สามารถบำรุงรักษาและทดสอบได้ง่าย (Testability)
- **2.4.1 ตรรกะทางธุรกิจที่ซับซ้อนทั้งหมด** (เช่น การเปลี่ยนสถานะ Workflow [cite: 3.5.3, 3.6.5], การบังคับใช้สิทธิ์ [cite: 4.4], การตรวจสอบ Deadline [cite: 3.2.5]) **จะถูกจัดการในฝั่ง Backend (NestJS)** [cite: 2.3] เพื่อให้สามารถบำรุงรักษาและทดสอบได้ง่าย (Testability)
- **2.4.2 Unified Workflow Engine (ใหม่):** รวม Logic การเดินเอกสารของ `CorrespondenceRouting` และ `RfaWorkflow` ให้ใช้ Core Engine เดียวกันเพื่อลดความซ้ำซ้อนและง่ายต่อการบำรุงรักษา
@@ -199,13 +199,13 @@
- Request for Method statement Approval (RFA_MES)
- Request for Material Approval (RFA_MAT)
- 3.5.2. การสร้างเอกสาร: ผู้ใช้ที่มีสิทธิ์ สามารถสร้างและแก้ไขได้
- 3.5.4. การอ้างอิงและจัดกลุ่ม: การจัดการ Drawing (RFA_DWG):
- 3.5.3. การอ้างอิงและจัดกลุ่ม: การจัดการ Drawing (RFA_DWG):
- เอกสาร RFA_DWG จะประกอบไปด้วย Shop Drawing (shop_drawings) หลายแผ่น ซึ่งแต่ละแผ่นมี Revision ของตัวเอง
- Shop Drawing แต่ละ Revision สามารถอ้างอิงถึง Contract Drawing (Ccontract_drawings) หลายแผ่น หรือไม่อ้างถึงก็ได้
- ระบบต้องมีส่วนสำหรับจัดการข้อมูล Master Data ของทั้ง Shop Drawing และ Contract Drawing แยกจากกัน
- 3.5.5. Workflow การอนุมัติ: ต้องรองรับกระบวนการอนุมัติที่ซับซ้อนและเป็นลำดับ เช่น
- 3.5.4. Workflow การอนุมัติ: ต้องรองรับกระบวนการอนุมัติที่ซับซ้อนและเป็นลำดับ เช่น
- ส่งจาก Originator -> Organization 1 -> Organization 2 -> Organization 3 แล้วส่งผลกลับตามลำดับเดิม (โดยถ้า องกรณ์ใดใน Workflow ให้ส่งกลับ ก็สามารถส่งผลกลับตามลำดับเดิมโดยไม่ต้องรอให้ถึง องกรณืในลำดับถัดไป)
- 3.5.6. การจัดการ: มีการจัดการอย่างน้อยดังนี้
- 3.5.5. การจัดการ: มีการจัดการอย่างน้อยดังนี้
- สามารถกำหนดวันแล้วเสร็จ (Deadline) สำหรับผู้รับผิดชอบของ องกรณ์ ที่อยู่ใน Workflow ได้
- มีระบบแจ้งเตือน ให้ผู้รับผิดชอบของ องกรณ์ ที่อยู่ใน Workflow ทราบ เมื่อมี RFA ใหม่ หรือมีการเปลี่ยนสถานะ
@@ -306,14 +306,10 @@
- ต้อง encrypt sensitive data ใน JSON fields
### **3.12 ข้อกำหนดพิเศษ**
- **3.12.1. การจัดการเอกสารโต้ตอบ (Correspondence Management)**
- ผู้ใช้งานที่มีสิทธิ์ระดับสูง (Superadmin) หรือผู้ได้รับอนุญาตเป็นกรณีพิเศษ สามารถเลือก 'สร้างในนามองค์กร (Create on behalf of)' ได้ เพื่อให้สามารถออกเลขที่เอกสาร (Running Number) ขององค์กรอื่นได้โดยไม่ต้องล็อกอินใหม่
- **3.12.2 การสร้างเอกสารขออนุมัติ**
- ผู้ใช้งานที่มีสิทธิ์ระดับสูง (Superadmin) หรือผู้ได้รับอนุญาตเป็นกรณีพิเศษ สามารถเลือก 'สร้างในนามองค์กร (Create on behalf of)' ได้ เพื่อให้สามารถออกเลขที่เอกสาร (Running Number) ขององค์กรอื่นได้โดยไม่ต้องล็อกอินใหม่
- **3.12.3 การจัดการเอกสารนำส่ง (Transmittals)**
- Workflow การอนุมัติ
- **3.12.4 ใบเวียนเอกสาร (Circulation Sheet)**
- Routing & Workflow
- **ผู้ใช้งานที่มีสิทธิ์ระดับสูง (Global) หรือผู้ได้รับอนุญาตเป็นกรณีพิเศษ**
- สามารถเลือก **สร้างในนามองค์กร (Create on behalf of)** ได้ เพื่อให้สามารถออกเลขที่เอกสาร (Running Number) ขององค์กรอื่นได้โดยไม่ต้องล็อกอินใหม่
- สามารถทำงานแทนผู้ใช้งานอื่นได้ Routing & Workflow ของ Correspondence, RFA, Circulation Sheet
## **🔐 4. ข้อกำหนดด้านสิทธิ์และการเข้าถึง (Access Control Requirements)**
### **4.1. ภาพรวม:** ผู้ใช้และองค์กรสามารถดูและแก้ไขเอกสารได้ตามสิทธิ์ที่ได้รับ โดยระบบสิทธิ์จะเป็นแบบ Role-Based Access Control (RBAC)
@@ -759,14 +755,14 @@
## **Document Control:**
- **Document:** Application Requirements Specification v1.4.2
- **Document:** Application Requirements Specification v1.4.3
- **Version:** 1.4
- **Date:** 2025-11-19
- **Date:** 2025-11-22
- **Author:** NAP LCBP3-DMS & Gemini
- **Status:** FINAL
- **Status:** FINAL-Rev.01
- **Classification:** Internal Technical Documentation
- **Approved By:** Nattanin
---
`End of Requirements Specification v1.4.2`
`End of Requirements Specification v1.4.3`

970
1_FullStackJS_V1_4_3.md Normal file
View File

@@ -0,0 +1,970 @@
# 📝 **Documents Management System Version 1.4.3: แนวทางการพัฒนา FullStackJS**
**สถานะ:** FINAL GUIDELINE Rev.01
**วันที่:** 2025-11-22
**อ้างอิง:** Requirements Specification v1.4.3
**Classification:** Internal Technical Documentation
## 🧠 **1. ปรัชญาทั่วไป (General Philosophy)**
แนวทางปฏิบัติที่ดีที่สุดแบบครบวงจรสำหรับการพัฒนา NestJS Backend, NextJS Frontend และ Tailwind-based UI/UX ในสภาพแวดล้อม TypeScript มุ่งเน้นที่ **"Data Integrity First"** (ความถูกต้องของข้อมูลต้องมาก่อน) ตามด้วย Security และ UX
* **ความชัดเจน (clarity), ความง่ายในการบำรุงรักษา (maintainability), ความสอดคล้องกัน (consistency) และ การเข้าถึงได้ (accessibility)** ตลอดทั้งสแต็ก
* **Strict Typing:** ใช้ TypeScript อย่างเคร่งครัด ห้าม `any`
* **Consistency:** ใช้ภาษาอังกฤษใน Code / ภาษาไทยใน Comment
* **Resilience:** ระบบต้องทนทานต่อ Network Failure และ Race Condition
## ⚙️ **2. แนวทางทั่วไปสำหรับ TypeScript**
### **2.1 หลักการพื้นฐาน**
* ใช้ **ภาษาอังกฤษ** สำหรับโค้ด
* ใช้ **ภาษาไทย** สำหรับ comment และเอกสารทั้งหมด
* กำหนดไทป์ (type) อย่างชัดเจนสำหรับตัวแปร, พารามิเตอร์ และค่าที่ส่งกลับ (return values) ทั้งหมด
* หลีกเลี่ยงการใช้ any; ให้สร้างไทป์ (types) หรืออินเทอร์เฟซ (interfaces) ที่กำหนดเอง
* ใช้ **JSDoc** สำหรับคลาส (classes) และเมธอด (methods) ที่เป็น public
* ส่งออก (Export) **สัญลักษณ์หลัก (main symbol) เพียงหนึ่งเดียว** ต่อไฟล์
* หลีกเลี่ยงบรรทัดว่างภายในฟังก์ชัน
* ระบุ // File: path/filename ในบรรทัดแรกของทุกไฟล์
* ระบุ // บันทึกการแก้ไข, หากมีการแก้ไขเพิ่มในอนาคต ให้เพิ่มบันทึก
### **2.2 Configuration & Secrets Management**
* **Production/Staging:** ห้ามใส่ Secrets (Password, Keys) ใน `docker-compose.yml` หลัก
* **Development:** ให้สร้างไฟล์ `docker-compose.override.yml` (เพิ่มใน `.gitignore`) เพื่อ Inject ตัวแปร Environment ที่เป็นความลับ
* **Validation:** ใช้ `joi` หรือ `zod` ในการ Validate Environment Variables ตอน Start App หากขาดตัวแปรสำคัญให้ Throw Error ทันที
### **2.3 Idempotency (ความสามารถในการทำซ้ำได้)**
* สำหรับการทำงานที่สำคัญ (Create Document, Approve, Transactional) **ต้อง** ออกแบบให้เป็น Idempotent
* Client **ต้อง** ส่ง Header `Idempotency-Key` (UUID) มากับ Request
* Server **ต้อง** ตรวจสอบว่า Key นี้เคยถูกประมวลผลสำเร็จไปแล้วหรือไม่ ถ้าใช่ ให้คืนค่าเดิมโดยไม่ทำซ้ำ
### **2.4 ข้อตกลงในการตั้งชื่อ (Naming Conventions)**
| Entity (สิ่งที่ตั้งชื่อ) | Convention (รูปแบบ) | Example (ตัวอย่าง) |
| :-------------------- | :----------------- | :--------------------------------- |
| Classes | PascalCase | UserService |
| Property | snake_case | user_id |
| Variables & Functions | camelCase | getUserInfo |
| Files & Folders | kebab-case | user-service.ts |
| Environment Variables | UPPERCASE | DATABASE_URL |
| Booleans | Verb + Noun | isActive, canDelete, hasPermission |
ใช้คำเต็ม — ไม่ใช้อักษรย่อ — ยกเว้นคำมาตรฐาน (เช่น API, URL, req, res, err, ctx)
### 🧩**2.5 ฟังก์ชัน (Functions)**
* เขียนฟังก์ชันให้สั้น และทำ **หน้าที่เพียงอย่างเดียว** (single-purpose) (\< 20 บรรทัด)
* ใช้ **early returns** เพื่อลดการซ้อน (nesting) ของโค้ด
* ใช้ **map**, **filter**, **reduce** แทนการใช้ loops เมื่อเหมาะสม
* ควรใช้ **arrow functions** สำหรับตรรกะสั้นๆ, และใช้ **named functions** ในกรณีอื่น
* ใช้ **default parameters** แทนการตรวจสอบค่า null
* จัดกลุ่มพารามิเตอร์หลายตัวให้เป็นอ็อบเจกต์เดียว (RO-RO pattern)
* ส่งค่ากลับ (Return) เป็นอ็อบเจกต์ที่มีไทป์กำหนด (typed objects) ไม่ใช่ค่าพื้นฐาน (primitives)
* รักษาระดับของสิ่งที่เป็นนามธรรม (abstraction level) ให้เป็นระดับเดียวในแต่ละฟังก์ชัน
### 🧱**2.6 การจัดการข้อมูล (Data Handling)**
* ห่อหุ้มข้อมูล (Encapsulate) ในไทป์แบบผสม (composite types)
* ใช้ **immutability** (การไม่เปลี่ยนแปลงค่า) ด้วย readonly และ as const
* ทำการตรวจสอบความถูกต้องของข้อมูล (Validations) ในคลาสหรือ DTOs ไม่ใช่ภายในฟังก์ชันทางธุรกิจ
* ตรวจสอบความถูกต้องของข้อมูลโดยใช้ DTOs ที่มีไทป์กำหนดเสมอ
### 🧰**2.7 คลาส (Classes)**
* ปฏิบัติตามหลักการ **SOLID**
* ควรใช้ **composition มากกว่า inheritance** (Prefer composition over inheritance)
* กำหนด **interfaces** สำหรับสัญญา (contracts)
* ให้คลาสมุ่งเน้นการทำงานเฉพาะอย่างและมีขนาดเล็ก (\< 200 บรรทัด, \< 10 เมธอด, \< 10 properties)
### 🚨**2.8 การจัดการข้อผิดพลาด (Error Handling)**
* ใช้ Exceptions สำหรับข้อผิดพลาดที่ไม่คาดคิด
* ดักจับ (Catch) ข้อผิดพลาดเพื่อแก้ไขหรือเพิ่มบริบท (context) เท่านั้น; หากไม่เช่นนั้น ให้ใช้ global error handlers
* ระบุข้อความข้อผิดพลาด (error messages) ที่มีความหมายเสมอ
### 🧪**2.9 การทดสอบ (ทั่วไป) (Testing (General))**
* ใช้รูปแบบ **ArrangeActAssert**
* ใช้ชื่อตัวแปรในการทดสอบที่สื่อความหมาย (inputData, expectedOutput)
* เขียน **unit tests** สำหรับ public methods ทั้งหมด
* จำลอง (Mock) การพึ่งพาภายนอก (external dependencies)
* เพิ่ม **acceptance tests** ต่อโมดูลโดยใช้รูปแบบ GivenWhen-Then
### **Testing Strategy โดยละเอียด**
* **Test Pyramid Structure**
/\
/ \ E2E Tests (10%)
/____\ Integration Tests (20%)
/ \ Unit Tests (70%)
/________\
* **Testing Tools Stack**
```typescript
// Backend Testing Stack
const backendTesting = {
unit: ['Jest', 'ts-jest', '@nestjs/testing'],
integration: ['Supertest', 'Testcontainers', 'Jest'],
e2e: ['Supertest', 'Jest', 'Database Seeds'],
security: ['Jest', 'Custom Security Test Helpers'],
performance: ['Jest', 'autocannon', 'artillery']
};
// Frontend Testing Stack
const frontendTesting = {
unit: ['Vitest', 'React Testing Library'],
integration: ['React Testing Library', 'MSW'],
e2e: ['Playwright', 'Jest'],
visual: ['Playwright', 'Loki']
};
```
* **Test Data Management**
```typescript
// Test Data Factories
interface TestDataFactory {
createUser(overrides?: Partial<User>): User;
createCorrespondence(overrides?: Partial<Correspondence>): Correspondence;
createRoutingTemplate(overrides?: Partial<RoutingTemplate>): RoutingTemplate;
}
// Test Scenarios
const testScenarios = {
happyPath: 'Normal workflow execution',
edgeCases: 'Boundary conditions and limits',
errorConditions: 'Error handling and recovery',
security: 'Authentication and authorization',
performance: 'Load and stress conditions'
};
```
## 🏗️ **3. แบ็กเอนด์ (NestJS) - Implementation Details**
### **3.1 หลักการ**
* **สถาปัตยกรรมแบบโมดูลาร์ (Modular architecture)**:
* หนึ่งโมดูลต่อหนึ่งโดเมน
* โครงสร้างแบบ Controller → Service → Repository (Model)
* API-First: มุ่งเน้นการสร้าง API ที่มีคุณภาพสูง มีเอกสารประกอบ (Swagger) ที่ชัดเจนสำหรับ Frontend Team
* DTOs ที่ตรวจสอบความถูกต้องด้วย **class-validator**
* ใช้ **MikroORM** (หรือ TypeORM/Prisma) สำหรับการคงอยู่ของข้อมูล (persistence) ซึ่งสอดคล้องกับสคีมา MariaDB
* ห่อหุ้มโค้ดที่ใช้ซ้ำได้ไว้ใน **common module** (@app/common):
* Configs, decorators, DTOs, guards, interceptors, notifications, shared services, types, validators
### **3.2 Database & Data Modeling (MariaDB + TypeORM)**
#### **3.2.1 Optimistic Locking & Versioning**
เพื่อป้องกัน Race Condition ในการแก้ไขข้อมูลพร้อมกัน (โดยเฉพาะการรันเลขที่เอกสาร) ให้เพิ่ม Column `@VersionColumn()` ใน Entity ที่สำคัญ
```typescript
@Entity()
export class DocumentCounter {
// ... fields
@Column()
last_number: number;
@VersionColumn() // เพิ่ม Versioning
version: number;
}
```
#### **3.2.2 Virtual Columns for JSON Performance**
เนื่องจากเราใช้ MariaDB 10.11 และมีการเก็บข้อมูล JSON (Details) ให้ใช้ **Generated Columns (Virtual)** สำหรับ Field ที่ต้อง Search/Sort บ่อยๆ และทำ Index บน Virtual Column นั้น
```sql
-- ตัวอย่าง SQL Migration
ALTER TABLE correspondence_revisions
ADD COLUMN ref_project_id INT GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(details, '$.projectId'))) VIRTUAL;
CREATE INDEX idx_ref_project_id ON correspondence_revisions(ref_project_id);
```
#### **3.2.3 Partitioning Strategy**
สำหรับตาราง `audit_logs` และ `notifications` ให้เตรียมออกแบบ Entity ให้รองรับ Partitioning (เช่น แยกตามปี) โดยใช้ Raw SQL Migration ในการสร้างตาราง
### **3.3 File Storage Service (Two-Phase Storage)**
ปรับปรุง Service จัดการไฟล์ให้รองรับ Transactional Integrity
1. **Upload (Phase 1):**
* รับไฟล์ → Scan Virus (ClamAV) → Save ลงโฟลเดอร์ `temp/`
* Return `temp_id` และ Metadata กลับไปให้ Client
2. **Commit (Phase 2):**
* เมื่อ Business Logic (เช่น Create Correspondence) ทำงานสำเร็จ
* Service จะย้ายไฟล์จาก `temp/` ไปยัง `permanent/{YYYY}/{MM}/`
* Update path ใน Database
* ทั้งหมดนี้ต้องอยู่ภายใต้ Database Transaction เดียวกัน (ถ้า DB Fail, ไฟล์จะค้างที่ Temp และถูกลบโดย Cron Job)
### **3.4 Document Numbering (Double-Lock Mechanism)**
การออกเลขที่เอกสารต้องใช้กลไกความปลอดภัย 2 ชั้น:
1. **Layer 1 (Redis Lock):** ใช้ `redlock` เพื่อ Block ไม่ให้ Process อื่นเข้ามายุ่งกับ Counter ของ Project/Type นั้นๆ ชั่วคราว
2. **Layer 2 (Optimistic Lock):** ตอน Update Database ให้เช็ค `version` ถ้า version เปลี่ยน (แสดงว่า Redis Lock หลุดหรือมีคนแทรก) ให้ Throw Error และ Retry ใหม่
### **3.5 Unified Workflow Engine**
ห้ามแยก Logic ระหว่าง `CorrespondenceRouting` และ `RfaWorkflow` ออกจากกันเด็ดขาด ให้สร้าง `WorkflowEngineService` ที่เป็น Generic:
* **Input:** `currentState`, `action`, `rules (Guard)`
* **Output:** `nextState`, `assignee`
* รองรับทั้ง Linear Flow (Routing) และ Complex Flow (RFA) ผ่าน Configuration
### **3.6 ฟังก์ชันหลัก (Core Functionalities)**
* Global **filters** สำหรับการจัดการ exception
* **Middlewares** สำหรับการจัดการ request
* **Guards** สำหรับการอนุญาต (permissions) และ RBAC
* **Interceptors** สำหรับการแปลงข้อมูล response และการบันทึก log
### **3.7 ข้อจำกัดในการ Deploy (QNAP Container Station)**
* **ห้ามใช้ไฟล์ .env** ในการตั้งค่า Environment Variables [cite: 2.1]
* การตั้งค่าทั้งหมด (เช่น Database connection string, JWT secret) **จะต้องถูกกำหนดผ่าน Environment Variable ใน docker-compose.yml โดยตรง** [cite: 6.5] ซึ่งจะจัดการผ่าน UI ของ QNAP Container Station [cite: 2.1]
### **3.8 ข้อจำกัดด้านความปลอดภัย (Security Constraints):**
* **File Upload Security:** ต้องมี virus scanning (ClamAV), file type validation (white-list), และ file size limits (50MB)
* **Input Validation:** ต้องป้องกัน OWASP Top 10 vulnerabilities (SQL Injection, XSS, CSRF)
* **Rate Limiting:** ต้อง implement rate limiting ตาม strategy ที่กำหนด
* **Secrets Management:** ต้องมี mechanism สำหรับจัดการ sensitive secrets อย่างปลอดภัย แม้จะใช้ docker-compose.yml
### **3.9 โครงสร้างโมดูลตามโดเมน (Domain-Driven Module Structure)**
เพื่อให้สอดคล้องกับสคีมา SQL (LCBP3-DMS) เราจะใช้โครงสร้างโมดูลแบบ **Domain-Driven (แบ่งตามขอบเขตธุรกิจ)** แทนการแบ่งตามฟังก์ชัน:
#### 3.9.1 **CommonModule:**
* เก็บ Services ที่ใช้ร่วมกัน เช่น DatabaseModule, FileStorageService (จัดการไฟล์ใน QNAP), AuditLogService, NotificationService
* จัดการ audit_logs
* NotificationService ต้องรองรับ Triggers ที่ระบุใน Requirement 6.7 [cite: 6.7]
#### 3.9.2 **AuthModule:**
* จัดการะการยืนยันตัวตน (JWT, Guards)
* **(สำคัญ)** ต้องรับผิดชอบการตรวจสอบสิทธิ์ **4 ระดับ** [cite: 4.2]: สิทธิ์ระดับระบบ (Global Role), สิทธิ์ระดับองกรณ์ (Organization Role), สิทธิ์ระดับโปรเจกต์ (Project Role), และ สิทธิ์ระดับสัญญา (Contract Role)
* **(สำคัญ)** ต้องมี API สำหรับ **Admin Panel** เพื่อ:
* สร้างและจัดการ Role และการจับคู่ Permission แบบไดนามิก [cite: 4.3]
* ให้ Superadmin สร้าง Organizations และกำหนด Org Admin ได้ [cite: 4.6]
* ให้ Superadmin/Admin จัดการ document_number_formats (รูปแบบเลขที่เอกสาร), document_number_counters (Running Number) [cite: 3.10]
#### 3.9.3 **UserModule:**
* จัดการ users, roles, permissions, global_default_roles, role_permissions, user_roles, user_project_roles
* **(สำคัญ)** ต้องมี API สำหรับ **Admin Panel** เพื่อ:
* สร้างและจัดการ Role และการจับคู่ Permission แบบไดนามิก [cite: 4.3]
#### 3.9.4 **ProjectModule:**
* จัดการ projects, organizations, contracts, project_parties, contract_parties
#### 3.9.5 **MasterModule:**
* จัดการ master data (correspondence_types, rfa_types, rfa_status_codes, rfa_approve_codes, circulation_status_codes, correspondence_types, correspondence_status, tags) [cite: 4.5]
#### 3.9.6 **CorrespondenceModule (โมดูลศูนย์กลาง):**
* จัดการ correspondences, correspondence_revisions, correspondence_tags
* **(สำคัญ)** Service นี้ต้อง Inject DocumentNumberingService เพื่อขอเลขที่เอกสารใหม่ก่อนการสร้าง
* **(สำคัญ)** ตรรกะการสร้าง/อัปเดต Revision จะอยู่ใน Service นี้
* จัดการ correspondence_attachments (ตารางเชื่อมไฟล์แนบ)
* รับผิดชอบ Routing **Correspondence Routing** (correspondence_routings, correspondence_routing_template_steps, correspondence_routing_templates, correspondence_status_transitions) สำหรับการส่งต่อเอกสารทั่วไประหว่างองค์กร
#### 3.9.7 **RfaModule:**
* จัดการ rfas, rfa_revisions, rfa_items
* รับผิดชอบเวิร์กโฟลว์ **"RFA Workflows"** (rfa_workflows, rfa_workflow_templates, rfa_workflow_template_steps, rfa_status_transitions) สำหรับการอนุมัติเอกสารทางเทคนิค
#### 3.9.8 **DrawingModule:**
* จัดการ shop_drawings, shop_drawing_revisions, contract_drawings, contract_drawing_volumes, contract_drawing_cats, contract_drawing_sub_cats, shop_drawing_main_categories, shop_drawing_sub_categories, contract_drawing_subcat_cat_maps, shop_drawing_revision_contract_refs
* จัดการ shop_drawing_revision_attachments และ contract_drawing_attachments(ตารางเชื่อมไฟล์แนบ)
#### 3.9.9 **CirculationModule:**
* จัดการ circulations, circulation_templates, circulation_assignees
* จัดการ circulation_attachments (ตารางเชื่อมไฟล์แนบ)
* รับผิดชอบเวิร์กโฟลว์ **"Circulations"** (circulation_status_transitions, circulation_template_assignees, circulation_assignees, circulation_recipients, circulation_actions, circulation_action_documents)สำหรับการเวียนเอกสาร **ภายในองค์กร**
#### 3.9.10 **TransmittalModule:**
* จัดการ transmittals และ transmittal_items
#### 3.9.11 **SearchModule:**
* ให้บริการค้นหาขั้นสูง (Advanced Search) [cite: 6.2] โดยใช้ **Elasticsearch** เพื่อรองรับการค้นหาแบบ Full-text จากชื่อเรื่อง, รายละเอียด, เลขที่เอกสาร, ประเภท, วันที่, และ Tags
* ระบบจะใช้ Elasticsearch Engine ในการจัดทำดัชนีเพื่อการค้นหาข้อมูลเชิงลึกจากเนื้อหาของเอกสาร โดยข้อมูลจะถูกส่งไปทำดัชนีจาก Backend (NestJS) ทุกครั้งที่มีการสร้างหรือแก้ไขเอกสาร
#### 3.9.12 **DocumentNumberingModule:**
* **สถานะ:** เป็น Module ภายใน (Internal Module) ไม่เปิด API สู่ภายนอก
* **หน้าที่:** ให้บริการ DocumentNumberingService ที่ Module อื่น (เช่น CorrespondenceModule) จะ Inject ไปใช้งาน
* **ตรรกะ:** รับผิดชอบการสร้างเลขที่เอกสารโดยใช้ **Redis distributed locking** แทน stored procedure
* **Features:**
* Application-level locking เพื่อป้องกัน race condition
* Retry mechanism ด้วย exponential backoff
* Fallback mechanism เมื่อการขอเลขล้มเหลว
* Audit log ทุกครั้งที่มีการ generate เลขที่เอกสารใหม่
#### 3.9.13 **CorrespondenceRoutingModule:**
* **สถานะ:** โมดูลหลักสำหรับจัดการการส่งต่อเอกสาร
* **หน้าที่:** จัดการแม่แบบการส่งต่อและการส่งต่อจริง
* **Entities:**
* CorrespondenceRoutingTemplate
* CorrespondenceRoutingTemplateStep
* CorrespondenceRouting
* **Features:**
* สร้างและจัดการแม่แบบการส่งต่อ
* ดำเนินการส่งต่อเอกสารตามแม่แบบ
* ติดตามสถานะการส่งต่อ
* คำนวณวันครบกำหนดอัตโนมัติ
* ส่งการแจ้งเตือนเมื่อมีการส่งต่อใหม่
#### 3.9.14 **WorkflowEngineModule:**
* **สถานะ:** Internal Module สำหรับจัดการ workflow logic
* **หน้าที่:** ประมวลผล state transitions และ business rules
* **Features:**
* State machine สำหรับสถานะเอกสาร
* Validation rules สำหรับการเปลี่ยนสถานะ
* Automatic status updates
* Deadline management และ escalation
#### 3.9.15 **JsonSchemaModule:**
* **สถานะ:** Internal Module สำหรับจัดการ JSON schemas
* **หน้าที่:** Validate, transform, และ manage JSON data structures
* **Features:**
* JSON schema validation ด้วย AJV
* Schema versioning และ migration
* Dynamic schema generation
* Data transformation และ sanitization
#### 3.9.16 **DetailsService:**
* **สถานะ:** Shared Service สำหรับจัดการ details fields
* **หน้าที่:** Centralized service สำหรับ JSON details operations
* **Methods:**
* validateDetails(type: string, data: any): ValidationResult
* transformDetails(input: any, targetVersion: string): any
* sanitizeDetails(data: any): any
* getDefaultDetails(type: string): any
### **3.10 สถาปัตยกรรมระบบ (System Architecture)**
โครงสร้างโมดูล (Module Structure)
```bash
📁 src
├── 📄 app.module.ts
├── 📄 main.ts
├── 📁 common # @app/common (โมดูลส่วนกลาง)
│ ├── 📁 auth # AuthModule (JWT, Guards)
│ ├── 📁 config # Configuration
│ ├── 📁 decorators # Custom Decorators (เช่น @RequirePermission)
│ ├── 📁 entities # Shared Entities (User, Role, Permission)
│ ├── 📁 exceptions # Global Exception Filters
│ ├── 📁 file-storage # FileStorageService (Virus Scanning, Security)
│ ├── 📁 guards # Custom Guards (RBAC Guard, RateLimitGuard)
│ ├── 📁 interceptors # Interceptors (Audit Log, Transform, Performance)
│ ├── 📁 resilience # Circuit Breaker, Retry Patterns
│ └── 📁 services # Shared Services (NotificationService, CachingService)
├── 📁 modules
│ ├── 📁 user # UserModule (จัดการ Users, Roles, Permissions)
│ ├── 📁 project # ProjectModule (จัดการ Projects, Organizations, Contracts)
│ ├── 📁 correspondence # CorrespondenceModule (จัดการเอกสารโต้ตอบ)
│ ├── 📁 rfa # RfaModule (จัดการเอกสารขออนุมัติ)
│ ├── 📁 drawing # DrawingModule (จัดการแบบแปลน)
│ ├── 📁 circulation # CirculationModule (จัดการใบเวียน)
│ ├── 📁 transmittal # TransmittalModule (จัดการเอกสารนำส่ง)
│ ├── 📁 search # SearchModule (ค้นหาขั้นสูงด้วย Elasticsearch)
│ ├── 📁 monitoring # MonitoringModule (Metrics, Health Checks)
│ └── 📁 document-numbering # DocumentNumberingModule (Internal Module)
└── 📁 database # Database Migration & Seeding Scripts
```
### **3.11 กลยุทธ์ความทนทานและการจัดการข้อผิดพลาด (Resilience & Error Handling Strategy)**
* **Circuit Breaker Pattern:** ใช้สำหรับ external service calls (Email, LINE, Elasticsearch)
* **Retry Mechanism:** ด้วย exponential backoff สำหรับ transient failures
* **Fallback Strategies:** Graceful degradation เมื่อบริการภายนอกล้มเหลว
* **Error Handling:** Error messages ต้องไม่เปิดเผยข้อมูล sensitive
* **Monitoring:** Centralized error monitoring และ alerting system
### **3.12 FileStorageService (ปรับปรุงใหม่):**
* **Virus Scanning:** Integrate ClamAV สำหรับ scan ไฟล์ที่อัปโหลดทั้งหมด
* **File Type Validation:** ใช้ white-list approach (PDF, DWG, DOCX, XLSX, ZIP)
* **File Size Limits:** 50MB ต่อไฟล์
* **Security Measures:**
* เก็บไฟล์นอก web root
* Download ผ่าน authenticated endpoint เท่านั้น
* Download links มี expiration time (24 ชั่วโมง)
* File integrity checks (checksum)
* Access control checks ก่อนดาวน์โหลด
### **3.13 เเทคโนโลยีที่ใช้ (Technology Stack)**
| ส่วน | Library/Tool | หมายเหตุ |
| ----------------------- | ---------------------------------------------------- | -------------------------------------- |
| **Framework** | `@nestjs/core`, `@nestjs/common` | Core Framework |
| **Language** | `TypeScript` | ใช้ TypeScript ทั้งระบบ |
| **Database** | `MariaDB 10.11` | ฐานข้อมูลหลัก |
| **ORM** | `@nestjs/typeorm`, `typeorm` | 🗃️จัดการการเชื่อมต่อและ Query ฐานข้อมูล |
| **Validation** | `class-validator`, `class-transformer` | 📦ตรวจสอบและแปลงข้อมูลใน DTO |
| **Auth** | `@nestjs/jwt`, `@nestjs/passport`, `passport-jwt` | 🔐การยืนยันตัวตนด้วย JWT |
| **Authorization** | `casl` | 🔐จัดการสิทธิ์แบบ RBAC |
| **File Upload** | `multer` | 📁จัดการการอัปโหลดไฟล์ |
| **Search** | `@nestjs/elasticsearch` | 🔍สำหรับการค้นหาขั้นสูง |
| **Notification** | `nodemailer` | 📬ส่งอีเมลแจ้งเตือน |
| **Scheduling** | `@nestjs/schedule` | 📬สำหรับ Cron Jobs (เช่น แจ้งเตือน Deadline) |
| **Logging** | `winston` | 📊บันทึก Log ที่มีประสิทธิภาพ |
| **Testing** | `@nestjs/testing`, `jest`, `supertest` | 🧪ทดสอบ Unit, Integration และ E2E |
| **Documentation** | `@nestjs/swagger` | 🌐สร้าง API Documentation อัตโนมัติ |
| **Security** | `helmet`, `rate-limiter-flexible` | 🛡️เพิ่มความปลอดภัยให้ API |
| **Resilience** | `@nestjs/circuit-breaker` | 🔄 Circuit breaker pattern |
| **Caching** | `@nestjs/cache-manager`, `cache-manager-redis-store` | 💾 Distributed caching |
| **Security** | `helmet`, `csurf`, `rate-limiter-flexible` | 🛡️ Security enhancements |
| **Validation** | `class-validator`, `class-transformer` | ✅ Input validation |
| **Monitoring** | `@nestjs/monitoring`, `winston` | 📊 Application monitoring |
| **File Processing** | `clamscan` | 🦠 Virus scanning |
| **Cryptography** | `bcrypt`, `crypto` | 🔐 Password hashing และ checksums |
| **JSON Validation** | `ajv`, `ajv-formats` | 🎯 JSON schema validation |
| **JSON Processing** | `jsonpath`, `json-schema-ref-parser` | 🔧 JSON manipulation |
| **Data Transformation** | `class-transformer` | 🔄 Object transformation |
| **Compression** | `compression` | 📦 JSON compression |
### **3.14 Security Testing:**
* **Penetration Testing:** ทดสอบ OWASP Top 10 vulnerabilities
* **Security Audit:** Review code สำหรับ security flaws
* **Virus Scanning Test:** ทดสอบ file upload security
* **Rate Limiting Test:** ทดสอบ rate limiting functionality
### **3.15 Performance Testing:**
* **Load Testing:** ทดสอบด้วย realistic workloads
* **Stress Testing:** หา breaking points ของระบบ
* **Endurance Testing:** ทดสอบการทำงานต่อเนื่องเป็นเวลานาน
### 🗄️**3.16 Backend State Management**
Backend (NestJS) ควรเป็น **Stateless** (ไม่เก็บสถานะ) "State" ทั้งหมดจะถูกจัดเก็บใน MariaDB
* **Request-Scoped State (สถานะภายใน Request เดียว):**
* **ปัญหา:** จะส่งต่อข้อมูล (เช่น User ที่ล็อกอิน) ระหว่าง Guard และ Service ใน Request เดียวกันได้อย่างไร?
* **วิธีแก้:** ใช้ **Request-Scoped Providers** ของ NestJS (เช่น AuthContextService) เพื่อเก็บข้อมูล User ปัจจุบันที่ได้จาก AuthGuard และให้ Service อื่น Inject ไปใช้
* **Application-Scoped State (การ Caching):**
* **ปัญหา:** ข้อมูล Master (เช่น roles, permissions, organizations) ถูกเรียกใช้บ่อย
* **วิธีแก้:** ใช้ **Caching** (เช่น @nestjs/cache-manager) เพื่อ Caching ข้อมูลเหล่านี้ และลดภาระ Database
### **3.17 Caching Strategy (ตามข้อ 6.4.2):**
* **Master Data Cache:** Roles, Permissions, Organizations (TTL: 1 hour)
* **User Session Cache:** User permissions และ profile (TTL: 30 minutes)
* **Search Result Cache:** Frequently searched queries (TTL: 15 minutes)
* **File Metadata Cache:** Attachment metadata (TTL: 1 hour)
* **Cache Invalidation:** Clear cache on update/delete operations
### **3.18 การไหลของข้อมูล (Data Flow)**
#### **3.18.1 Main Flow:**
1. Request: ผ่าน Nginx Proxy Manager -> NestJS Controller
2. **Rate Limiting:** RateLimitGuard ตรวจสอบ request limits
3. **Input Validation:** Validation Pipe ตรวจสอบและ sanitize inputs
4. Authentication: JWT Guard ตรวจสอบ Token และดึงข้อมูล User
5. Authorization: RBAC Guard ตรวจสอบสิทธิ์
6. **Security Checks:** Virus scanning (สำหรับ file upload), XSS protection
7. Business Logic: Service Layer ประมวลผลตรรกะทางธุรกิจ
8. **Resilience:** Circuit breaker และ retry logic สำหรับ external calls
9. Data Access: Repository Layer ติดต่อกับฐานข้อมูล
10. **Caching:** Cache frequently accessed data
11. **Audit Log:** บันทึกการกระทำสำคัญ
12. Response: ส่งกลับไปยัง Frontend
#### **3.18.2 Workflow Data Flow:**
1. User สร้างเอกสาร → เลือก routing template
2. System สร้าง routing instances ตาม template
3. สำหรับแต่ละ routing step:
- กำหนด due date (จาก expected_days)
- ส่ง notification ไปยังองค์กรผู้รับ
- อัพเดทสถานะเป็น SENT
4. เมื่อองค์กรผู้รับดำเนินการ:
- อัพเดทสถานะเป็น ACTIONED/FORWARDED/REPLIED
- บันทึก processed_by และ processed_at
- ส่ง notification ไปยังขั้นตอนต่อไป (ถ้ามี)
5. เมื่อครบทุกขั้นตอน → อัพเดทสถานะเอกสารเป็น COMPLETED
#### **3.18.3 JSON Details Processing Flow:**
1. **Receive Request** → Get JSON data from client
2. **Schema Validation** → Validate against predefined schema
3. **Data Sanitization** → Sanitize and transform data
4. **Version Check** → Handle schema version compatibility
5. **Storage** → Store validated JSON in database
6. **Retrieval** → Retrieve and transform on demand
### 📊**3.19 Monitoring & Observability (ตามข้อ 6.8)**
#### **Application Monitoring:**
* **Health Checks:** `/health` endpoint สำหรับ load balancer
* **Metrics Collection:** Response times, error rates, throughput
* **Distributed Tracing:** สำหรับ request tracing across services
* **Log Aggregation:** Structured logging ด้วย JSON format
* **Alerting:** สำหรับ critical errors และ performance degradation
#### **Business Metrics:**
* จำนวน documents created ต่อวัน
* Workflow completion rates
* User activity metrics
* System utilization rates
* Search query performance
#### **Performance Targets:**
* API Response Time: < 200ms (90th percentile)
* Search Query Performance: < 500ms
* File Upload Performance: < 30 seconds สำหรับไฟล์ 50MB
* Cache Hit Ratio: > 80%
## 🖥️ **4. ฟรอนต์เอนด์ (Next.js) - Implementation Details**
**โปรไฟล์นักพัฒนา (Developer Profile:** วิศวกร TypeScript + React/NextJS ระดับ Senior
เชี่ยวชาญ TailwindCSS, Shadcn/UI, และ Radix สำหรับการพัฒนา UI
### **4.1 State Management & Offline Support**
#### **4.1.1 Auto-Save Drafts**
ใช้ `zustand` ร่วมกับ middleware `persist` (ลง LocalStorage) สำหรับฟอร์มที่มีขนาดใหญ่ (RFA, Correspondence) เพื่อป้องกันข้อมูลหายเมื่อเน็ตหลุด
```typescript
// lib/stores/draft-store.ts
export const useDraftStore = create(
persist(
(set) => ({
drafts: {},
saveDraft: (key, data) => set((state) => ({ drafts: { ...state.drafts, [key]: data } })),
clearDraft: (key) => set((state) => {
const newDrafts = { ...state.drafts };
delete newDrafts[key];
return { drafts: newDrafts };
}),
}),
{ name: 'form-drafts' }
)
);
```
### **4.2 Dynamic Form Generator**
เพื่อรองรับ JSON Schema หลากหลายรูปแบบ ให้สร้าง Component กลางที่รับ Schema แล้ว Gen Form ออกมา (ลดการแก้ Code บ่อยๆ)
* **Libraries:** แนะนำ `react-jsonschema-form` หรือสร้าง Wrapper บน `react-hook-form` ที่ Recursively render field ตาม Type
* **Validation:** ใช้ `ajv` ที่ฝั่ง Client เพื่อ Validate JSON ก่อน Submit
### **4.3 Mobile Responsiveness (Card View)**
ตารางข้อมูล (`DataTable`) ต้องมีความฉลาดในการแสดงผล:
* **Desktop:** แสดงเป็น Table ปกติ
* **Mobile:** แปลงเป็น **Card View** โดยอัตโนมัติ (ซ่อน Header, แสดง Label คู่ Value ในแต่ละ Card)
```tsx
// components/ui/responsive-table.tsx
<div className="hidden md:block">
<Table>{/* Desktop View */}</Table>
</div>
<div className="md:hidden space-y-4">
{data.map((item) => (
<Card key={item.id}>
{/* Mobile View: Render cells as list items */}
</Card>
))}
</div>
```
### **4.4 Optimistic Updates**
ใช้ความสามารถของ **TanStack Query** (`onMutate`) เพื่ออัปเดต UI ทันที (เช่น เปลี่ยนสถานะจาก "รออ่าน" เป็น "อ่านแล้ว") แล้วค่อยส่ง Request ไป Server ถ้า Failed ค่อย Rollback
### **4.5 แนวทางการพัฒนาโค้ด (Code Implementation Guidelines)**
* ใช้ **early returns** เพื่อความชัดเจน
* ใช้คลาสของ **TailwindCSS** ในการกำหนดสไตล์เสมอ
* ควรใช้ class: syntax แบบมีเงื่อนไข (หรือ utility clsx) มากกว่าการใช้ ternary operators ใน class strings
* ใช้ **const arrow functions** สำหรับ components และ handlers
* Event handlers ให้ขึ้นต้นด้วย handle... (เช่น handleClick, handleSubmit)
* รวมแอตทริบิวต์สำหรับการเข้าถึง (accessibility) ด้วย:
tabIndex="0", aria-label, onKeyDown, ฯลฯ
* ตรวจสอบให้แน่ใจว่าโค้ดทั้งหมด **สมบูรณ์**, **ผ่านการทดสอบ**, และ **ไม่ซ้ำซ้อน (DRY)**
* ต้อง import โมดูลที่จำเป็นต้องใช้อย่างชัดเจนเสมอ
### **4.6 UI/UX ด้วย React**
* ใช้ **semantic HTML**
* ใช้คลาสของ **Tailwind** ที่รองรับ responsive (sm:, md:, lg:)
* รักษาลำดับชั้นของการมองเห็น (visual hierarchy) ด้วยการใช้ typography และ spacing
* ใช้ **Shadcn** components (Button, Input, Card, ฯลฯ) เพื่อ UI ที่สอดคล้องกัน
* ทำให้ components มีขนาดเล็กและมุ่งเน้นการทำงานเฉพาะอย่าง
* ใช้ utility classes สำหรับการจัดสไตล์อย่างรวดเร็ว (spacing, colors, text, ฯลฯ)
* ตรวจสอบให้แน่ใจว่าสอดคล้องกับ **ARIA** และใช้ semantic markup
### **4.7 การตรวจสอบฟอร์มและข้อผิดพลาด (Form Validation & Errors)**
* ใช้ไลบรารีฝั่ง client เช่น zod และ react-hook-form
* แสดงข้อผิดพลาดด้วย **alert components** หรือข้อความ inline
* ต้องมี labels, placeholders, และข้อความ feedback
### **🧪4.8 Frontend Testing**
เราจะใช้ **React Testing Library (RTL)** สำหรับการทดสอบ Component และ **Playwright** สำหรับ E2E:
* **Unit Tests (การทดสอบหน่วยย่อย):**
* **เครื่องมือ:** Vitest + RTL
* **เป้าหมาย:** ทดสอบ Component ขนาดเล็ก (เช่น Buttons, Inputs) หรือ Utility functions
* **Integration Tests (การทดสอบการบูรณาการ):**
* **เครื่องมือ:** RTL + **Mock Service Worker (MSW)**
* **เป้าหมาย:** ทดสอบว่า Component หรือ Page ทำงานกับ API (ที่จำลองขึ้น) ได้ถูกต้อง
* **เทคนิค:** ใช้ MSW เพื่อจำลอง NestJS API และทดสอบว่า Component แสดงผลข้อมูลจำลองได้ถูกต้องหรือไม่ (เช่น ทดสอบหน้า Dashboard [cite: 5.3] ที่ดึงข้อมูลจาก v_user_tasks)
* **E2E (End-to-End) Tests:**
* **เครื่องมือ:** **Playwright**
* **เป้าหมาย:** ทดสอบ User Flow ทั้งระบบโดยอัตโนมัติ (เช่น ล็อกอิน -> สร้าง RFA -> ตรวจสอบ Workflow Visualization [cite: 5.6])
### **🗄4.9 Frontend State Management**
สำหรับ Next.js App Router เราจะแบ่ง State เป็น 4 ระดับ:
1. **Local UI State (สถานะ UI ชั่วคราว):**
* **เครื่องมือ:** useState, useReducer
* **ใช้เมื่อ:** จัดการสถานะเล็กๆ ที่จบใน Component เดียว (เช่น Modal เปิด/ปิด, ค่าใน Input)
2. **Server State (สถานะข้อมูลจากเซิร์ฟเวอร์):**
* **เครื่องมือ:** **React Query (TanStack Query)** หรือ SWR
* **ใช้เมื่อ:** จัดการข้อมูลที่ดึงมาจาก NestJS API (เช่น รายการ correspondences, rfas, drawings)
* **ทำไม:** React Query เป็น "Cache" ที่จัดการ Caching, Re-fetching, และ Invalidation ให้โดยอัตโนมัติ
3. **Global Client State (สถานะส่วนกลางฝั่ง Client):**
* **เครื่องมือ:** **Zustand** (แนะนำ) หรือ Context API
* **ใช้เมื่อ:** จัดการข้อมูลที่ต้องใช้ร่วมกันทั่วทั้งแอป และ *ไม่ใช่* ข้อมูลจากเซิร์ฟเวอร์ (เช่น ข้อมูล User ที่ล็อกอิน, สิทธิ์ Permissions)
4. **Form State (สถานะของฟอร์ม):**
* **เครื่องมือ:** **React Hook Form** + **Zod**
* **ใช้เมื่อ:** จัดการฟอร์มที่ซับซ้อน (เช่น ฟอร์มสร้าง RFA, ฟอร์ม Circulation [cite: 3.7])
## 🔐 **5. Security & Access Control (Full Stack Integration)**
### **5.1 CASL Integration (Shared Ability)**
* **Backend:** ใช้ CASL กำหนด Permission Rule
* **Frontend:** ให้ดึง Rule (JSON) จาก Backend มา Load ใส่ `@casl/react` เพื่อให้ Logic การ Show/Hide ปุ่ม ตรงกัน 100%
### **5.2 Maintenance Mode**
เพิ่ม Middleware (ทั้ง NestJS และ Next.js) เพื่อตรวจสอบ Flag ใน Redis:
* ถ้า `MAINTENANCE_MODE = true`
* **API:** Return `503 Service Unavailable` (ยกเว้น Admin IP)
* **Frontend:** Redirect ไปหน้า `/maintenance`
### **5.3 Idempotency Client**
สร้าง Axios Interceptor เพื่อ Generate `Idempotency-Key` สำหรับ POST/PUT/DELETE requests ทุกครั้ง
```typescript
// lib/api/client.ts
import { v4 as uuidv4 } from 'uuid';
apiClient.interceptors.request.use((config) => {
if (['post', 'put', 'delete'].includes(config.method)) {
config.headers['Idempotency-Key'] = uuidv4();
}
return config;
});
```
### **5.4 RBAC และการควบคุมสิทธิ์ (RBAC & Permission Control)**
ใช้ Decorators เพื่อบังคับใช้สิทธิ์การเข้าถึง โดยอ้างอิงสิทธิ์จากตาราง permissions
```typescript
@RequirePermission('rfas.respond') // ต้องตรงกับ 'permission_code'
@Put(':id')
updateRFA(@Param('id') id: string) {
return this.rfaService.update(id);
}
```
#### **5.4.1 Roles (บทบาท)**
* **Superadmin**: ไม่มีข้อจำกัดใดๆ [cite: 4.3]
* **Admin**: มีสิทธิ์เต็มที่ในองค์กร [cite: 4.3]
* **Document Control**: เพิ่ม/แก้ไข/ลบ เอกสารในองค์กร [cite: 4.3]
* **Editor**: สามารถ เพิ่ม/แก้ไข เอกสารที่กำหนด [cite: 4.3]
* **Viewer**: สามารถดู เอกสาร [cite: 4.3]
#### **5.4.2 ตัวอย่าง Permissions (จากตาราง permissions)**
* rfas.view, rfas.create, rfas.respond, rfas.delete
* drawings.view, drawings.upload, drawings.delete
* corr.view, corr.manage
* transmittals.manage
* cirs.manage
* project_parties.manage
การจับคู่ระหว่าง roles และ permissions **เริ่มต้น** จะถูก seed ผ่านสคริปต์ (ดังที่เห็นในไฟล์ SQL)**อย่างไรก็ตาม AuthModule/UserModule ต้องมี API สำหรับ Admin เพื่อสร้าง Role ใหม่และกำหนดสิทธิ์ (Permissions) เพิ่มเติมได้ในภายหลัง** [cite: 4.3]
## 📊 **6. Notification & Background Jobs**
### **6.1 Digest Notification**
ห้ามส่ง Email ทันทีที่เกิด Event ให้:
1. Push Event ลง Queue (Redis/BullMQ)
2. มี Processor รอเวลา (เช่น 5 นาที) เพื่อ Group Events ที่คล้ายกัน (เช่น "คุณมีเอกสารรออนุมัติ 5 ฉบับ")
3. ส่ง Email เดียว (Digest) เพื่อลด Spam
## 🔗 **7. แนวทางการบูรณาการ Full Stack (Full Stack Integration Guidelines)**
| Aspect (แง่มุม) | Backend (NestJS) | Frontend (NextJS) | UI Layer (Tailwind/Shadcn) |
| :----------------------- | :------------------------- | :---------------------------- | :------------------------------------- |
| API | REST / GraphQL Controllers | API hooks ผ่าน fetch/axios/SWR | Components ที่รับข้อมูล |
| Validation (การตรวจสอบ) | class-validator DTOs | zod / react-hook-form | สถานะของฟอร์ม/input ใน Shadcn |
| Auth (การยืนยันตัวตน) | Guards, JWT | NextAuth / cookies | สถานะ UI ของ Auth (loading, signed in) |
| Errors (ข้อผิดพลาด) | Global filters | Toasts / modals | Alerts / ข้อความ feedback |
| Testing (การทดสอบ) | Jest (unit/e2e) | Vitest / Playwright | Visual regression |
| Styles (สไตล์) | Scoped modules (ถ้าจำเป็น) | Tailwind / Shadcn | Tailwind utilities |
| Accessibility (การเข้าถึง) | Guards + filters | ARIA attributes | Semantic HTML |
## 🗂️ **8. ข้อตกลงเฉพาะสำหรับ DMS (LCBP3-DMS)**
ส่วนนี้ขยายแนวทาง FullStackJS ทั่วไปสำหรับโปรเจกต์ **LCBP3-DMS** โดยมุ่งเน้นไปที่เวิร์กโฟลว์การอนุมัติเอกสาร (Correspondence, RFA, Drawing, Contract, Transmittal, Circulation)
### 🧾**8.1 มาตรฐาน AuditLog (AuditLog Standard)**
บันทึกการดำเนินการ CRUD และการจับคู่ทั้งหมดลงในตาราง audit_logs
| Field (ฟิลด์) | Type (จาก SQL) | Description (คำอธิบาย) |
| :----------- | :------------- | :----------------------------------------------- |
| audit_id | BIGINT | Primary Key |
| user_id | INT | ผู้ใช้ที่ดำเนินการ (FK -> users) |
| action | VARCHAR(100) | rfa.create, correspondence.update, login.success |
| entity_type | VARCHAR(50) | ชื่อตาราง/โมดูล เช่น 'rfa', 'correspondence' |
| entity_id | VARCHAR(50) | Primary ID ของระเบียนที่ได้รับผลกระทบ |
| details_json | JSON | ข้อมูลบริบท (เช่น ฟิลด์ที่มีการเปลี่ยนแปลง) |
| ip_address | VARCHAR(45) | IP address ของผู้ดำเนินการ |
| user_agent | VARCHAR(255) | User Agent ของผู้ดำเนินการ |
| created_at | TIMESTAMP | Timestamp (UTC) |
### 📂**8.2 การจัดการไฟล์ (File Handling)**
#### **8.2.1 มาตรฐานการอัปโหลดไฟล์ (File Upload Standard)**
* **Security-First Approach:** การอัปโหลดไฟล์ทั้งหมดจะถูกจัดการโดย FileStorageService ที่มี security measures ครบถ้วน
* ไฟล์จะถูกเชื่อมโยงไปยัง Entity ที่ถูกต้องผ่าน **ตารางเชื่อม (Junction Tables)** เท่านั้น:
* correspondence_attachments (เชื่อม Correspondence กับ Attachments)
* circulation_attachments (เชื่อม Circulation กับ Attachments)
* shop_drawing_revision_attachments (เชื่อม Shop Drawing Revision กับ Attachments)
* contract_drawing_attachments (เชื่อม Contract Drawing กับ Attachments)
* เส้นทางจัดเก็บไฟล์ (Upload path): อ้างอิงจาก Requirement 2.1 คือ /share/dms-data [cite: 2.1] โดย FileStorageService จะสร้างโฟลเดอร์ย่อยแบบรวมศูนย์ (เช่น /share/dms-data/uploads/{YYYY}/{MM}/[stored_filename])
* ประเภทไฟล์ที่อนุญาต: pdf, dwg, docx, xlsx, zip (ผ่าน white-list validation)
* ขนาดสูงสุด: **50 MB**
* จัดเก็บนอก webroot
* ให้บริการไฟล์ผ่าน endpoint ที่ปลอดภัย /files/:attachment_id/download
#### **8.2.2 Security Controls สำหรับ File Access:**
การเข้าถึงไฟล์ไม่ใช่การเข้าถึงโดยตรง endpoint /files/:attachment_id/download จะต้อง:
1. ค้นหาระเบียน attachment
2. ตรวจสอบว่า attachment_id นี้ เชื่อมโยงกับ Entity ใด (เช่น correspondence, circulation, shop_drawing_revision, contract_drawing) ผ่านตารางเชื่อม
3. ตรวจสอบว่าผู้ใช้มีสิทธิ์ (permission) ในการดู Entity ต้นทางนั้นๆ หรือไม่
4. ตรวจสอบ download token expiration (24 ชั่วโมง)
5. บันทึก audit log การดาวน์โหลด
### 🔟**8.3 การจัดการเลขที่เอกสาร (Document Numbering) [cite: 3.10]**
* **เป้าหมาย:** สร้างเลขที่เอกสาร (เช่น correspondence_number) โดยอัตโนมัติ ตามรูปแบบที่กำหนด
* **ตรรกะการนับ:** การนับ Running number (SEQ) จะนับแยกตาม Key: **Project + Originator Organization + Document Type + Year**
* **ตาราง SQL:**
* document_number_formats: Admin ใช้กำหนด "รูปแบบ" (Template) ของเลขที่ (เช่น {ORG_CODE}-{TYPE_CODE}-{YEAR_SHORT}-{SEQ:4}) โดยกำหนดตาม **Project** และ **Document Type** [cite: 4.5]
* document_number_counters: ระบบใช้เก็บ "ตัวนับ" ล่าสุดของ Key (Project+Org+Type+Year)
* **การทำงาน (Backend):**
* DocumentNumberingModule จะให้บริการ DocumentNumberingService
* เมื่อ CorrespondenceModule ต้องการสร้างเอกสารใหม่, มันจะเรียก documentNumberingService.generateNextNumber(...)
* Service นี้จะใช้ **Redis distributed locking** แทน stored procedure ซึ่งจะจัดการ Database Transaction และ Row Locking ภายใน Application Layer เพื่อรับประกันการป้องกัน Race Condition
* มี retry mechanism และ fallback strategies
### 📊**8.4 การรายงานและการส่งออก (Reporting & Exports)**
#### **8.4.1 วิวสำหรับการรายงาน (Reporting Views) (จาก SQL)**
การรายงานควรสร้างขึ้นจาก Views ที่กำหนดไว้ล่วงหน้าในฐานข้อมูลเป็นหลัก:
* v_current_correspondences: สำหรับ revision ปัจจุบันทั้งหมดของเอกสารที่ไม่ใช่ RFA
* v_current_rfas: สำหรับ revision ปัจจุบันทั้งหมดของ RFA และข้อมูล master
* v_contract_parties_all: สำหรับการตรวจสอบความสัมพันธ์ของ project/contract/organization
* v_user_tasks: สำหรับ Dashboard "งานของฉัน"
* v_audit_log_details: สำหรับ Activity Feed
Views เหล่านี้ทำหน้าที่เป็นแหล่งข้อมูลหลักสำหรับการรายงานฝั่งเซิร์ฟเวอร์และการส่งออกข้อมูล
#### **8.4.2 กฎการส่งออก (Export Rules)**
* Export formats: CSV, Excel, PDF.
* จัดเตรียมมุมมองสำหรับพิมพ์ (Print view).
* รวมลิงก์ไปยังต้นทาง (เช่น /rfas/:id).
## 🧮 **9. ฟรอนต์เอนด์: รูปแบบ DataTable และฟอร์ม (Frontend: DataTable & Form Patterns)**
### **9.1 DataTable (ServerSide)**
* Endpoint: /api/{module}?page=1&pageSize=20&sort=...&filter=...
* ต้องรองรับ: การแบ่งหน้า (pagination), การเรียงลำดับ (sorting), การค้นหา (search), การกรอง (filters)
* แสดง revision ล่าสุดแบบ inline เสมอ (สำหรับ RFA/Drawing)
### **9.2 มาตรฐานฟอร์ม (Form Standards)**
* ต้องมีการใช้งาน Dropdowns แบบขึ้นต่อกัน (Dependent dropdowns) (ตามที่สคีมารองรับ):
* Project → Contract Drawing Volumes
* Contract Drawing Category → Sub-Category
* RFA (ประเภท Shop Drawing) → Shop Drawing Revisions ที่เชื่อมโยงได้
* **File Upload Security:** ต้องรองรับ **Multi-file upload (Drag-and-Drop)** [cite: 5.7] พร้อม virus scanning feedback
* **File Type Indicators:** UI ต้องอนุญาตให้ผู้ใช้กำหนดว่าไฟล์ใดเป็น **"เอกสารหลัก"** หรือ "เอกสารแนบประกอบ" [cite: 5.7] พร้อมแสดง file type icons
* **Security Feedback:** แสดง security warnings สำหรับ file types ที่เสี่ยงหรือ files ที่ fail virus scan
* ส่ง (Submit) ผ่าน API พร้อม feedback แบบ toast
### **9.3 ข้อกำหนด Component เฉพาะ (Specific UI Requirements)**
* **Dashboard - My Tasks:** ต้องพัฒนา Component ตาราง "งานของฉัน" (My Tasks)ซึ่งดึงข้อมูลงานที่ผู้ใช้ล็อกอินอยู่ต้องรับผิดชอบ (Main/Action) จาก v_user_tasks [cite: 5.3]
* **Workflow Visualization:** ต้องพัฒนา Component สำหรับแสดงผล Workflow (โดยเฉพาะ RFA)ที่แสดงขั้นตอนทั้งหมดเป็นลำดับ โดยขั้นตอนปัจจุบัน (active) เท่านั้นที่ดำเนินการได้ และขั้นตอนอื่นเป็น disabled [cite: 5.6] ต้องมีตรรกะสำหรับ Admin ในการ override หรือย้อนกลับขั้นตอนได้ [cite: 5.6]
* **Admin Panel:** ต้องมีหน้า UI สำหรับ Superadmin/Admin เพื่อจัดการข้อมูลหลัก (Master Data [cite: 4.5]), การเริ่มต้นใช้งาน (Onboarding [cite: 4.6]), และ **รูปแบบเลขที่เอกสาร (Numbering Formats [cite: 3.10])**
* **Security Dashboard:** แสดง security metrics และ audit logs สำหรับ administrators
## 🧭 **10. แดชบอร์ดและฟีดกิจกรรม (Dashboard & Activity Feed)**
### **10.1 การ์ดบนแดชบอร์ด (Dashboard Cards)**
* แสดง Correspondences, RFAs, Circulations, Shop Drawing Revision ล่าสุด
* รวมสรุป KPI (เช่น "RFAs ที่รอการอนุมัติ", "Shop Drawing ที่รอการอนุมัติ") [cite: 5.3]
* รวมลิงก์ด่วนไปยังโมดูลต่างๆ
* **Security Metrics:** แสดงจำนวน files scanned, security incidents, failed login attempts
### **10.2 ฟีดกิจกรรม (Activity Feed)**
* แสดงรายการ v_audit_log_details ล่าสุด (10 รายการ) ที่เกี่ยวข้องกับผู้ใช้
* รวม security-related activities (failed logins, permission changes)
```typescript
// ตัวอย่าง API response
[
{ user: 'editor01', action: 'Updated RFA (LCBP3-RFA-001)', time: '2025-11-04T09:30Z' },
{ user: 'system', action: 'Virus scan completed - 0 threats found', time: '2025-11-04T09:25Z' }
]
```
## 🛡️ **11. ข้อกำหนดที่ไม่ใช่ฟังก์ชันการทำงาน (Non-Functional Requirements)**
ส่วนนี้สรุปข้อกำหนด Non-Functional จาก requirements.md เพื่อให้ทีมพัฒนาทาน
* **Audit Log [cite: 6.1]:** ทุกการกระทำที่สำคัญ (C/U/D) ต้องถูกบันทึกใน audit_logs
* **Performance [cite: 6.4]:** ต้องใช้ Caching สำหรับข้อมูลที่เรียกบ่อย และใช้ Pagination
* **Security [cite: 6.5]:** ต้องมี Rate Limiting และจัดการ Secret ผ่าน docker-compose.yml (ไม่ใช่ .env)
* **File Security [cite: 3.9.6]:** ต้องมี virus scanning, file type validation, access controls
* **Resilience [cite: 6.5.3]:** ต้องมี circuit breaker, retry mechanisms, graceful degradation
* **Backup & Recovery [cite: 6.6]:** ต้องมีแผนสำรองข้อมูลทั้ง Database (MariaDB) และ File Storage (/share/dms-data) อย่างน้อยวันละ 1 ครั้ง
* **Notification Strategy [cite: 6.7]:** ระบบแจ้งเตือน (Email/Line) ต้องถูก Trigger เมื่อมีเอกสารใหม่ส่งถึง, มีการมอบหมายงานใหม่ (Circulation), หรือ (ทางเลือก) เมื่องานเสร็จ/ใกล้ถึงกำหนด
* **Monitoring [cite: 6.8]:** ต้องมี health checks, metrics collection, alerting
## ✅ **12. มาตรฐานที่นำไปใช้แล้ว (จาก SQL v1.4.0) (Implemented Standards (from SQL v1.4.0))**
ส่วนนี้ยืนยันว่าแนวทางปฏิบัติที่ดีที่สุดต่อไปนี้เป็นส่วนหนึ่งของการออกแบบฐานข้อมูลอยู่แล้ว และควรถูกนำไปใช้ประโยชน์ ไม่ใช่สร้างขึ้นใหม่
***Soft Delete:** นำไปใช้แล้วผ่านคอลัมน์ deleted_at ในตารางสำคัญ (เช่น correspondences, rfas, project_parties) ตรรกะการดึงข้อมูลต้องกรอง deleted_at IS NULL
***Database Indexes:** สคีมาได้มีการทำ index ไว้อย่างหนักหน่วงบน foreign keys และคอลัมน์ที่ใช้ค้นหาบ่อย (เช่น idx_rr_rfa, idx_cor_project, idx_cr_is_current) เพื่อประสิทธิภาพ
***โครงสร้าง RBAC:** มีระบบ users, roles, permissions, user_roles, และ user_project_roles ที่ครอบคลุมอยู่แล้ว
***Data Seeding:** ข้อมูล Master (roles, permissions, organization_roles, initial users, project parties) ถูกรวมอยู่ในสคริปต์สคีมาแล้ว
***Application-level Locking:** ใช้ Redis distributed lock แทน stored procedure
***File Security:** Virus scanning, file type validation, access control
***Resilience Patterns:** Circuit breaker, retry, fallback mechanisms
***Security Measures:** Input validation, rate limiting, security headers
***Monitoring:** Health checks, metrics collection, distributed tracing
## 🧩 **13. การปรับปรุงที่แนะนำ (สำหรับอนาคต) (Recommended Enhancements (Future))**
* ✅ สร้าง Background job (โดยใช้ **n8n** เพื่อเชื่อมต่อกับ **Line** [cite: 2.7] และ/หรือใช้สำหรับการแจ้งเตือน RFA ที่ใกล้ถึงกำหนด due_date [cite: 6.7])
* ✅ เพิ่ม job ล้างข้อมูลเป็นระยะสำหรับ attachments ที่ไม่ถูกเชื่อมโยงกับ Entity ใดๆ เลย (ไฟล์กำพร้า)
* 🔄 **AI-Powered Document Classification:** ใช้ machine learning สำหรับ automatic document categorization
* 🔄 **Advanced Analytics:** Predictive analytics สำหรับ workflow optimization
* 🔄 **Mobile App:** Native mobile application สำหรับ field workers
* 🔄 **Blockchain Integration:** สำหรับ document integrity verification ที่ต้องการความปลอดภัยสูงสุด
## ✅ **14. Summary Checklist for Developers**
ก่อนส่ง PR (Pull Request) นักพัฒนาต้องตรวจสอบหัวข้อต่อไปนี้:
* [ ] **Security:** ไม่มี Secrets ใน Code, ใช้ `docker-compose.override.yml` แล้ว
* [ ] **Concurrency:** ใช้ Optimistic Lock ใน Entity ที่เสี่ยง Race Condition แล้ว
* [ ] **Idempotency:** API รองรับ Idempotency Key แล้ว
* [ ] **File Upload:** ใช้ Flow Two-Phase (Temp -> Perm) แล้ว
* [ ] **Mobile:** หน้าจอแสดงผลแบบ Card View บนมือถือได้ถูกต้อง
* [ ] **Performance:** สร้าง Index สำหรับ JSON Virtual Columns แล้ว (ถ้ามี)
---
## 📋 **15. Summary of Key Changes from Previous Version**
### **Security Enhancements:**
1. **File Upload Security** - Virus scanning, file type validation, access controls
2. **Input Validation** - OWASP Top 10 protection, XSS/CSRF prevention
3. **Rate Limiting** - Comprehensive rate limiting strategy
4. **Secrets Management** - Secure handling of sensitive configuration
### **Architecture Improvements:**
1. **Document Numbering** - Changed from Stored Procedure to Application-level Locking
2. **Resilience Patterns** - Circuit breaker, retry mechanisms, fallback strategies
3. **Monitoring & Observability** - Health checks, metrics, distributed tracing
4. **Caching Strategy** - Comprehensive caching with proper invalidation
### **Performance Targets :**
1. **API Response Time** - < 200ms (90th percentile)
2. **Search Performance** - < 500ms
3. **File Upload** - < 30 seconds for 50MB files
4. **Cache Hit Ratio** - > 80%
### **Operational Excellence:**
1. **Disaster Recovery** - RTO < 4 hours, RPO < 1 hour
2. **Backup Procedures** - Comprehensive backup and restoration
3. **Security Testing** - Penetration testing and security audits
4. **Performance Testing** - Load testing with realistic workloads
เอกสารนี้สะท้อนถึงความมุ่งมั่นในการสร้างระบบที่มีความปลอดภัย, มีความทนทาน, และมีประสิทธิภาพสูง พร้อมรองรับการเติบโตในอนาคตและความต้องการทางธุรกิจที่เปลี่ยนแปลงไป
**หมายเหตุ:** แนวทางนี้จะถูกทบทวนและปรับปรุงเป็นระยะตาม feedback จากทีมพัฒนาและความต้องการทางธุรกิจที่เปลี่ยนแปลงไป
## **Document Control:**
* **Document:** FullStackJS v1.4.3
* **Version:** 1.4
* **Date:** 2025-11-22
* **Author:** NAP LCBP3-DMS & Gemini
* **Status:** FINAL
* **Classification:** Internal Technical Documentation
* **Approved By:** Nattanin
---
`End of FullStackJS Guidelines v1.4.3`

552
2_Backend_Plan_V1_4_3.md Normal file
View File

@@ -0,0 +1,552 @@
# 📋 **แผนการพัฒนา Backend (NestJS) - LCBP3-DMS v1.4.3 (ฉบับปรับปรุง)**
**สถานะ:** FINAL GUIDELINE Rev.01
**วันที่:** 2025-11-22
**อ้างอิง:** Requirements v1.4.3 & FullStackJS Guidelines v1.4.3
**Classification:** Internal Technical Documentation
-----
## 🎯 **ภาพรวมโครงการ**
พัฒนา Backend สำหรับระบบบริหารจัดการเอกสารโครงการ (Document Management System) ที่มีความปลอดภัยสูง รองรับการทำงานพร้อมกัน (Concurrency) ได้อย่างถูกต้องแม่นยำ มีสถาปัตยกรรมที่ยืดหยุ่นต่อการขยายตัว และรองรับการจัดการเอกสารที่ซับซ้อน มีระบบ Workflow การอนุมัติ และการควบคุมสิทธิ์แบบ RBAC 4 ระดับ พร้อมมาตรการความปลอดภัยที่ทันสมัย
-----
## 📐 **สถาปัตยกรรมระบบ**
### **Technology Stack (Updated)**
- **Framework:** NestJS (TypeScript, ESM)
- **Database:** MariaDB 10.11 (ใช้ Virtual Columns)
- **ORM:** TypeORM (ใช้ Optimistic Locking)
- **Authentication:** JWT + Passport
- **Authorization:** CASL (RBAC 4-level)
- **File Upload:** Multer + Virus Scanning (ClamAV) + Two-Phase Storage
- **Search:** Elasticsearch
- **Notification:** Nodemailer + n8n (Line Integration) + BullMQ Queue
- **Caching/Locking:** Redis (Redlock) สำหรับ Distributed Locking
- **Queue:** BullMQ (Redis) สำหรับ Notification Batching และ Async Jobs
- **Resilience:** Circuit Breaker, Retry Patterns
- **Security:** Helmet, CSRF Protection, Rate Limiting, Idempotency
- **Monitoring:** Winston, Health Checks, Metrics
- **Scheduling:** @nestjs/schedule (Cron Jobs)
- **Documentation:** Swagger
- **Validation:** Zod / Class-validator
### **โครงสร้างโมดูล (Domain-Driven)**
```tree
src/
├── common/ # Shared Module
│ ├── auth/ # JWT, Guards, RBAC
│ ├── config/ # Configuration Management
│ ├── decorators/ # @RequirePermission, @RateLimit
│ ├── entities/ # Base Entities
│ ├── exceptions/ # Global Filters
│ ├── file-storage/ # FileStorageService (Virus Scanning + Two-Phase)
│ ├── guards/ # RBAC Guard, RateLimitGuard
│ ├── interceptors/ # Audit, Transform, Performance, Idempotency
│ ├── resilience/ # Circuit Breaker, Retry Patterns
│ ├── security/ # Input Validation, XSS Protection
│ ├── idempotency/ # [New] Idempotency Logic
│ └── maintenance/ # [New] Maintenance Mode Guard
├── modules/
│ ├── user/ # Users, Roles, Permissions
│ ├── project/ # Projects, Contracts, Organizations
│ ├── master/ # Master Data Management
│ ├── correspondence/ # Correspondence Management
│ ├── rfa/ # RFA & Workflows
│ ├── drawing/ # Shop/Contract Drawings
│ ├── circulation/ # Internal Circulation
│ ├── transmittal/ # Transmittals
│ ├── search/ # Elasticsearch
│ ├── monitoring/ # Metrics, Health Checks
│ ├── workflow-engine/ # [New] Unified Workflow Logic
│ ├── document-numbering/ # [Update] Double-Locking Logic
│ ├── notification/ # [Update] Queue & Digest
│ └── file-storage/ # [Update] Two-Phase Commit
└── database/ # Migrations & Seeds
```
-----
## 🗓️ **แผนการพัฒนาแบบ Phase-Based**
- *(Dependency Diagram ถูกละไว้เพื่อประหยัดพื้นที่ เนื่องจากมีการอ้างอิงจากแผนเดิม)*
## **Phase 0: Infrastructure & Configuration (สัปดาห์ที่ 1)**
**Milestone:** โครงสร้างพื้นฐานพร้อม รองรับ Secrets ที่ปลอดภัย และ Redis พร้อมใช้งาน
### **Phase 0: Tasks**
- **[ ] T0.1 Secure Configuration Setup**
- [ ] ปรับปรุง `ConfigModule` ให้รองรับการอ่านค่าจาก Environment Variables
- [ ] สร้าง Template `docker-compose.override.yml.example` สำหรับ Dev
- [ ] Validate Config ด้วย Joi/Zod ตอน Start App (Throw error ถ้าขาด Secrets)
- [ ] **Security:** Setup network segmentation และ firewall rules
- [ ] **Deliverable:** Configuration Management พร้อมใช้งานอย่างปลอดภัย
- [ ] **Dependencies:** None (Task เริ่มต้น)
- **[ ] T0.2 Redis & Queue Infrastructure**
- [ ] Setup Redis Container
- [ ] Setup BullMQ Module ใน NestJS สำหรับจัดการ Background Jobs
- [ ] Setup Redis Client สำหรับ Distributed Lock (Redlock)
- [ ] **Security:** Setup Redis authentication และ encryption
- [ ] **Deliverable:** Redis และ Queue System พร้อมใช้งาน
- [ ] **Dependencies:** T0.1
- **[ ] T0.3 Setup Database Connection**
- [ ] Import SQL Schema v1.4.2 เข้า MariaDB
- [ ] Run Seed Data (organizations, users, roles, permissions)
- [ ] Configure TypeORM ใน AppModule
- [ ] **Security:** Setup database connection encryption
- [ ] ทดสอบ Connection
- [ ] **Deliverable:** Database พร้อมใช้งาน, มี Seed Data
- [ ] **Dependencies:** T0.1
- **[ ] T0.4 Setup Git Repository**
- [ ] สร้าง Repository ใน Gitea (git.np-dms.work)
- [ ] Setup .gitignore, README.md, SECURITY.md
- [ ] Commit Initial Project
- [ ] **Deliverable:** Code อยู่ใน Version Control
- [ ] **Dependencies:** T0.1, T0.2, T0.3
-----
## **Phase 1: Core Foundation & Security (สัปดาห์ที่ 2-3)**
**Milestone:** ระบบ Authentication, Authorization, Idempotency พื้นฐาน และ Security Baseline
### **Phase 1: Tasks**
- **[ ] T1.1 CommonModule - Base Infrastructure**
- [ ] สร้าง Base Entity (id, created\_at, updated\_at, deleted\_at)
- [ ] สร้าง Global Exception Filter (ไม่เปิดเผย sensitive information)
- [ ] สร้าง Response Transform Interceptor
- [ ] สร้าง Audit Log Interceptor
- [ ] **[New] Idempotency Interceptor:** ตรวจสอบ Header `Idempotency-Key` และ Cache Response เดิมใน Redis
- [ ] **[New] Maintenance Mode Middleware:** ตรวจสอบ Flag ใน **Redis Key** เพื่อ Block API ระหว่างปรับปรุงระบบ **(Admin ใช้ Redis/Admin UI ในการ Toggle สถานะ)**
- [ ] สร้าง RequestContextService - สำหรับเก็บข้อมูลระหว่าง Request
- [ ] สร้าง ConfigService - Centralized configuration management
- [ ] สร้าง CryptoService - สำหรับ encryption/decryption
- [ ] **Security:** Implement input validation pipeline
- [ ] **Deliverable:** Common Services พร้อมใช้ รวมถึง Idempotency และ Maintenance Mode
- [ ] **Dependencies:** T0.2, T0.3
- **[ ] T1.2 AuthModule - JWT Authentication**
- [ ] สร้าง Entity: User
- [ ] สร้าง AuthService:
- [ ] login(username, password) → JWT Token
- [ ] validateUser(username, password) → User | null
- [ ] Password Hashing (bcrypt) + salt
- [ ] สร้าง JWT Strategy (Passport)
- [ ] สร้าง JwtAuthGuard
- [ ] สร้าง Controllers:
- [ ] POST /auth/login → { access\_token, refresh\_token }
- [ ] POST /auth/register → Create User (Admin only)
- [ ] POST /auth/refresh → Refresh token
- [ ] POST /auth/logout → Revoke token
- [ ] GET /auth/profile (Protected)
- [ ] **Security:** Implement rate limiting สำหรับ authentication endpoints
- [ ] **Deliverable:** ล็อกอิน/ล็อกเอาต์ทำงานได้อย่างปลอดภัย
- [ ] **Dependencies:** T1.1, T0.3
- **[ ] T1.3 UserModule - User Management**
- [ ] สร้าง Entities: User, Role, Permission, UserRole, UserAssignment, **UserPreference**
- [ ] สร้าง UserService CRUD (พร้อม soft delete)
- [ ] สร้าง RoleService CRUD
- [ ] สร้าง PermissionService (Read-Only, จาก Seed)
- [ ] สร้าง UserAssignmentService - สำหรับจัดการ user assignments ตาม scope
- [ ] สร้าง **UserPreferenceService** - สำหรับจัดการการตั้งค่า Notification และ UI
- [ ] สร้าง Controllers:
- [ ] GET /users → List Users (Paginated)
- [ ] GET /users/:id → User Detail
- [ ] POST /users → Create User (ต้องบังคับเปลี่ยน password ครั้งแรก)
- [ ] PUT /users/:id → Update User
- [ ] DELETE /users/:id → Soft Delete
- [ ] GET /roles → List Roles
- [ ] POST /roles → Create Role (Admin)
- [ ] PUT /roles/:id/permissions → Assign Permissions
- [ ] **Security:** Implement permission checks สำหรับ user management
- [ ] **Deliverable:** จัดการผู้ใช้, Role, และ Preferences ได้
- [ ] **Dependencies:** T1.1, T1.2
- **[ ] T1.4 RBAC Guard - 4-Level Authorization**
- [ ] สร้าง @RequirePermission() Decorator
- [ ] สร้าง RbacGuard ที่ตรวจสอบ 4 ระดับ:
- [ ] Global Permissions
- [ ] Organization Permissions
- [ ] Project Permissions
- [ ] Contract Permissions
- [ ] Permission Hierarchy Logic
- [ ] Integration กับ CASL
- [ ] **Security:** Implement audit logging สำหรับ permission checks
- [ ] **Deliverable:** ระบบสิทธิ์ทำงานได้ทั้ง 4 ระดับ
- [ ] **Dependencies:** T1.1, T1.3
- **[ ] T1.5 ProjectModule - Base Structures**
- [ ] สร้าง Entities:
- [ ] Organization
- [ ] Project
- [ ] Contract
- [ ] ProjectOrganization (Junction)
- [ ] ContractOrganization (Junction)
- [ ] สร้าง Services & Controllers:
- [ ] GET /organizations → List
- [ ] POST /projects → Create (Superadmin)
- [ ] GET /projects/:id/contracts → List Contracts
- [ ] POST /projects/:id/contracts → Create Contract
- [ ] **Security:** Implement data isolation ระหว่าง organizations
- [ ] **Deliverable:** จัดการโครงสร้างโปรเจกต์ได้
- [ ] **Dependencies:** T1.1, T1.2, T0.3
-----
## **Phase 2: High-Integrity Data & File Management (สัปดาห์ที่ 4)**
**Milestone:** Master Data, ระบบจัดการไฟล์แบบ Transactional, Document Numbering ที่ไม่มี Race Condition, JSON details system พร้อมใช้งาน
### **Phase 2: Tasks**
- **[ ] T2.1 Virtual Columns for JSON**
- [ ] ออกแบบ Migration Script สำหรับตารางที่มี JSON Details
- [ ] เพิ่ม **Generated Columns (Virtual)** สำหรับฟิลด์ที่ใช้ Search บ่อยๆ (เช่น `project_id`, `type`) พร้อม Index
- [ ] **Security:** Implement admin-only access สำหรับ master data
- [ ] **Deliverable:** JSON Data Search Performance ดีขึ้น
- [ ] **Dependencies:** T0.3, T1.1, T1.5
- **[ ] T2.2 FileStorageService - Two-Phase Storage**
- [ ] สร้าง Attachment Entity
- [ ] สร้าง FileStorageService:
- [ ] **Phase 1 (Upload):** API รับไฟล์ → Scan Virus → Save ลง `temp/` → Return `temp_id`
- [ ] **Phase 2 (Commit):** Method `commitFiles(tempIds[])` → ย้ายจาก `temp/` ไป `permanent/{YYYY}/{MM}/` → Update DB
- [ ] File type validation (white-list: PDF, DWG, DOCX, XLSX, PPTX, ZIP)
- [ ] File size check (max 50MB)
- [ ] Generate checksum (SHA-256)
- [ ] **Cleanup Job:** สร้าง Cron Job ลบไฟล์ใน `temp/` ที่ค้างเกิน 24 ชม. **โดยตรวจสอบจากคอลัมน์ `expires_at` ในตาราง `attachments`**
- [ ] สร้าง Controller:
- [ ] POST /files/upload → { temp\_id } (Protected)
- [ ] POST /files/commit → { attachment\_id, url } (Protected)
- [ ] GET /files/:id/download → File Stream (Protected + Expiration)
- [ ] **Security:** Access Control - ตรวจสอบสิทธิ์ผ่าน Junction Table
- [ ] **Deliverable:** อัปโหลด/ดาวน์โหลดไฟล์ได้อย่างปลอดภัย แบบ Transactional
- [ ] **Dependencies:** T1.1, T1.4
- **[ ] T2.3 DocumentNumberingModule - Double-Lock Mechanism**
- [ ] สร้าง Entities:
- [ ] DocumentNumberFormat
- [ ] DocumentNumberCounter
- [ ] สร้าง DocumentNumberingService:
- [ ] generateNextNumber(projectId, orgId, typeId, year) → string
- [ ] ใช้ **Double-Lock Mechanism**:
1. Acquire **Redis Lock** (Key: `doc_num:{project}:{type}`)
2. Read DB & Calculate Next Number
3. Update DB with **Optimistic Lock** Check (ใช้ `@VersionColumn()`)
4. Release Redis Lock
5. Retry on Failure ด้วย exponential backoff
- [ ] Fallback mechanism เมื่อการขอเลขล้มเหลว
- [ ] Format ตาม Template: {ORG\_CODE}-{TYPE\_CODE}-{YEAR\_SHORT}-{SEQ:4}
- **ไม่มี Controller** (Internal Service เท่านั้น)
- [ ] **Security:** Implement audit log ทุกครั้งที่มีการ generate เลขที่
- [ ] **Deliverable:** Service สร้างเลขที่เอกสารได้ถูกต้องและปลอดภัย ไม่มี Race Condition
- [ ] **Dependencies:** T1.1, T0.3
- **[ ] T2.4 SecurityModule - Enhanced Security**
- [ ] สร้าง Input Validation Service:
- [ ] XSS Prevention
- [ ] SQL Injection Prevention
- [ ] CSRF Protection
- [ ] สร้าง RateLimitGuard:
- [ ] Implement rate limiting ตาม strategy (anonymous: 100/hr, authenticated: 500-5000/hr)
- [ ] Different limits สำหรับ endpoints ต่างๆ
- [ ] สร้าง Security Headers Middleware
- [ ] **Security:** Implement content security policy (CSP)
- [ ] **Deliverable:** Security layers ทำงานได้
- [ ] **Dependencies:** T1.1
- **[ ] T2.5 JSON Details & Schema Management**
- [ ] T2.5.1 JsonSchemaModule - Schema Management: สร้าง Service สำหรับ Validate, get, register JSON schemas
- [ ] T2.5.2 DetailsService - Data Processing: สร้าง Service สำหรับ sanitize, transform, compress/decompress JSON
- [ ] T2.5.3 JSON Security & Validation: Implement security checks และ validation rules
- [ ] **Deliverable:** JSON schema system ทำงานได้
- [ ] **Dependencies:** T1.1
-----
## **Phase 3: Unified Workflow Engine (สัปดาห์ที่ 5-6)**
**Milestone:** ระบบ Workflow กลางที่รองรับทั้ง Routing ปกติ และ RFA
### **Phase 3: Tasks**
- **[ ] T3.1 WorkflowEngineModule (New)**
- [ ] ออกแบบ Generic Schema สำหรับ Workflow State Machine
- [ ] Implement Service: `initializeWorkflow()`, `processAction()`, `getNextStep()`
- [ ] รองรับ Logic การ "ข้ามขั้นตอน" และ "ส่งกลับ" ภายใน Engine เดียว
- [ ] **Security:** Implement audit logging สำหรับ workflow actions
- [ ] **Deliverable:** Unified Workflow Engine พร้อมใช้งาน
- [ ] **Dependencies:** T1.1
- **[ ] T3.2 CorrespondenceModule - Basic CRUD**
- [ ] สร้าง Entities (Correspondence, Revision, Recipient, Tag, Reference, Attachment)
- [ ] สร้าง CorrespondenceService (Create with Document Numbering, Update with new Revision, Soft Delete)
- [ ] สร้าง Controllers (POST/GET/PUT/DELETE /correspondences)
- [ ] [New] Implement Impersonation Logic: ตรวจสอบ originatorId ใน DTO หากมีการส่งมา ต้องเช็คว่า User ปัจจุบันมีสิทธิ์กระทำการแทนหรือไม่ (Superadmin)
- [ ] **Security:** Implement permission checks สำหรับ document access
- [ ] **Deliverable:** สร้าง/แก้ไข/ดูเอกสารได้
- [ ] **Dependencies:** T1.1, T1.2, T1.3, T1.4, T1.5, T2.3, T2.2, T2.5
- **[ ] T3.3 CorrespondenceModule - Advanced Features**
- [ ] Implement Status Transitions (DRAFT → SUBMITTED)
- [ ] Implement References (Link Documents)
- [ ] Implement Search (Basic)
- [ ] **Security:** Implement state transition validation
- [ ] **Deliverable:** Workflow พื้นฐานทำงานได้
- [ ] **Dependencies:** T3.2
- **[ ] T3.4 Correspondence Integration with Workflow**
- [ ] เชื่อมต่อ `CorrespondenceService` เข้ากับ `WorkflowEngineModule`
- [ ] ย้าย Logic การ Routing เดิมมาใช้ Engine ใหม่
- [ ] สร้าง API endpoints สำหรับ Frontend (Templates, Pending Tasks, Bulk Action)
- [ ] **Security:** Implement permission checks สำหรับ workflow operations
- [ ] **Deliverable:** ระบบส่งต่อเอกสารทำงานได้สมบูรณ์ด้วย Unified Engine
- [ ] **Dependencies:** T3.1, T3.2
-----
## **Phase 4: Drawing & Advanced Workflows (สัปดาห์ที่ 7-8)**
**Milestone:** การจัดการแบบและ RFA โดยใช้ Unified Engine
### **Phase 4: Tasks**
- **[ ] T4.1 DrawingModule - Contract Drawings**
- [ ] สร้าง Entities (ContractDrawing, Volume, Category, SubCategory, Attachment)
- [ ] สร้าง ContractDrawingService CRUD
- [ ] สร้าง Controllers (GET/POST /drawings/contract)
- [ ] **Security:** Implement access control สำหรับ contract drawings
- [ ] **Deliverable:** จัดการ Contract Drawings ได้
- [ ] **Dependencies:** T1.1, T1.2, T1.4, T1.5, T2.2
- **[ ] T4.2 DrawingModule - Shop Drawings**
- [ ] สร้าง Entities (ShopDrawing, Revision, Main/SubCategory, ContractRef, RevisionAttachment)
- [ ] สร้าง ShopDrawingService CRUD (รวมการสร้าง Revision)
- [ ] สร้าง Controllers (GET/POST /drawings/shop, /drawings/shop/:id/revisions)
- [ ] Link Shop Drawing Revision → Contract Drawings
- [ ] **Security:** Implement virus scanning สำหรับ drawing files
- [ ] **Deliverable:** จัดการ Shop Drawings และ Revisions ได้
- [ ] **Dependencies:** T4.1
- **[ ] T5.1 RfaModule with Unified Workflow**
- [ ] สร้าง Entities (Rfa, RfaRevision, RfaItem, RfaWorkflowTemplate/Step)
- [ ] สร้าง RfaService (Create RFA, Link Shop Drawings)
- [ ] Implement RFA Workflow โดยใช้ Configuration ของ `WorkflowEngineModule`
- [ ] สร้าง Controllers (POST/GET /rfas, POST /rfas/:id/workflow/...)
- [ ] **Resilience:** Implement circuit breaker สำหรับ notification services
- [ ] **Deliverable:** RFA Workflow ทำงานได้ด้วย Unified Engine
- [ ] **Dependencies:** T3.2, T4.2, T2.5, T6.2
-----
## **Phase 5: Workflow Systems & Resilience (สัปดาห์ที่ 8-9)**
**Milestone:** ระบบ Workflow ทั้งหมดพร้อม Resilience Patterns
### **Phase 5: Tasks**
- **[ ] T5.2 CirculationModule - Internal Routing**
- [ ] สร้าง Entities (Circulation, Template, Routing, Attachment)
- [ ] สร้าง CirculationService (Create 1:1 with Correspondence, Assign User, Complete/Close Step)
- [ ] สร้าง Controllers (POST/GET /circulations, POST /circulations/:id/steps/...)
- [ ] **Resilience:** Implement retry mechanism สำหรับ assignment notifications
- [ ] **Deliverable:** ใบเวียนภายในองค์กรทำงานได้
- [ ] **Dependencies:** T3.2, T2.5, T6.2
- **[ ] T5.3 TransmittalModule - Document Forwarding**
- [ ] สร้าง Entities (Transmittal, TransmittalItem)
- [ ] สร้าง TransmittalService (Create Correspondence + Transmittal, Link Multiple Correspondences)
- [ ] สร้าง Controllers (POST/GET /transmittals)
- [ ] **Security:** Implement access control สำหรับ transmittal items
- [ ] **Deliverable:** สร้าง Transmittal ได้
- [ ] **Dependencies:** T3.2
-----
## **Phase 6: Notification & Resilience (สัปดาห์ที่ 9)**
**Milestone:** ระบบแจ้งเตือนแบบ Digest และการจัดการข้อมูลขนาดใหญ่
### **Phase 6: Tasks**
- **[ ] T6.1 SearchModule - Elasticsearch Integration**
- [ ] Setup Elasticsearch Container
- [ ] สร้าง SearchService (index/update/delete documents, search)
- [ ] Index ทุก Document Type
- [ ] สร้าง Controllers (GET /search)
- [ ] **Resilience:** Implement circuit breaker สำหรับ Elasticsearch
- [ ] **Deliverable:** ค้นหาขั้นสูงทำงานได้
- [ ] **Dependencies:** T3.2, T5.1, T4.2, T5.2, T5.3
- **[ ] T6.2 Notification Queue & Digest**
- [ ] สร้าง NotificationService (sendEmail/Line/System)
- [ ] **Producer:** Push Event ลง BullMQ Queue
- [ ] **Consumer:** จัดกลุ่ม Notification (Digest Message) และส่งผ่าน Email/Line
- [ ] Integrate กับ Workflow Events (แจ้ง Recipients, Assignees, Deadline)
- [ ] สร้าง Controllers (GET /notifications, PUT /notifications/:id/read)
- [ ] **Resilience:** Implement retry mechanism ด้วย exponential backoff
- [ ] **Deliverable:** ระบบแจ้งเตือนทำงานได้แบบ Digest
- [ ] **Dependencies:** T1.1, T6.4
- **[ ] T6.3 MonitoringModule - Observability**
- [ ] สร้าง Health Check Controller (GET /health)
- [ ] สร้าง Metrics Service (API response times, Error rates)
- [ ] สร้าง Performance Interceptor (Track request duration)
- [ ] สร้าง Logging Service (Structured logging)
- [ ] **Deliverable:** Monitoring system ทำงานได้
- [ ] **Dependencies:** T1.1
- **[ ] T6.4 ResilienceModule - Circuit Breaker & Retry**
- [ ] สร้าง Circuit Breaker Service (@CircuitBreaker() decorator)
- [ ] สร้าง Retry Service (@Retry() decorator)
- [ ] สร้าง Fallback Strategies
- [ ] Implement สำหรับ Email, LINE, Elasticsearch, Virus Scanning
- [ ] **Deliverable:** Resilience patterns ทำงานได้
- [ ] **Dependencies:** T1.1
- **[ ] T6.5 Data Partitioning Strategy**
- [ ] ออกแบบ Table Partitioning สำหรับ `audit_logs` และ `notifications` (แบ่งตาม Range: Year)
- [ ] เขียน Raw SQL Migration สำหรับสร้าง Partition Table
- [ ] **Deliverable:** Database Performance และ Scalability ดีขึ้น
- [ ] **Dependencies:** T0.3
-----
## **Phase 7: Testing & Hardening (สัปดาห์ที่ 10-12)**
**Milestone:** ทดสอบความทนทานต่อ Race Condition, Security, และ Performance
### **Phase 7: Tasks**
- **[ ] T7.1 Concurrency Testing**
- [ ] เขียน Test Scenarios ยิง Request ขอเลขที่เอกสารพร้อมกัน 100 Request (ต้องไม่ซ้ำและไม่ข้าม)
- [ ] ทดสอบ Optimistic Lock ทำงานถูกต้องเมื่อ Redis ถูกปิด
- [ ] ทดสอบ File Upload พร้อมกันหลายไฟล์
- [ ] **Deliverable:** ระบบทนทานต่อ Concurrency Issues
- **[ ] T7.2 Transaction Integrity Testing**
- [ ] ทดสอบ Upload ไฟล์แล้ว Kill Process ก่อน Commit
- [ ] ทดสอบ Two-Phase File Storage ทำงานถูกต้อง
- [ ] ทดสอบ Database Transaction Rollback Scenarios
- [ ] **Deliverable:** Data Integrity รับประกันได้
- **[ ] T7.3 Security & Idempotency Test**
- [ ] ทดสอบ Replay Attack โดยใช้ `Idempotency-Key` ซ้ำ
- [ ] ทดสอบ Maintenance Mode Block API ได้จริง
- [ ] ทดสอบ RBAC 4-Level ทำงานถูกต้อง 100%
- [ ] **Deliverable:** Security และ Idempotency ทำงานได้ตาม设计要求
- **[ ] T7.4 Unit Testing (80% Coverage)**
- **[ ] T7.5 Integration Testing**
- **[ ] T7.6 E2E Testing**
- **[ ] T7.7 Performance Testing**
- [ ] Load Testing: 100 concurrent users
- [ ] **(สำคัญ)** การจูนและทดสอบ Load Test จะต้องทำในสภาพแวดล้อมที่จำลอง Spec ของ QNAP Server (TS-473A, AMD Ryzen V1500B) เพื่อให้ได้ค่า Response Time และ Connection Pool ที่เที่ยงตรง
- [ ] Stress Testing
- [ ] Endurance Testing
- [ ] **Deliverable:** Performance targets บรรลุ
- **[ ] T7.8 Security Testing**
- [ ] Penetration Testing (OWASP Top 10)
- [ ] Security Audit (Code review, Dependency scanning)
- [ ] File Upload Security Testing
- [ ] **Deliverable:** Security tests ผ่าน
- **[ ] T7.9 Performance Optimization**
- [ ] Implement Caching (Master Data, User Permissions, Search Results)
- [ ] Database Optimization (Review Indexes, Query Optimization, Pagination)
- [ ] **Deliverable:** Response Time \< 200ms (90th percentile)
-----
## **Phase 8: Documentation & Deployment (สัปดาห์ที่ 14)**
**Milestone:** เอกสารและ Deploy สู่ Production พร้อม Security Hardening
### **Phase 8: Tasks**
- **[ ] T8.1 API Documentation (Swagger)**
- **[ ] T8.2 Technical Documentation**
- **[ ] T8.3 Security Hardening**
- **[ ] T8.4 Deployment Preparation (QNAP Setup, Nginx Proxy Manager)**
- **[ ] T8.5 Production Deployment**
- **[ ] T8.6 Handover to Frontend Team**
-----
## 📊 **สรุป Timeline**
| Phase | ระยะเวลา | จำนวนงาน | Output หลัก |
| :------ | :----------- | :----------- | :--------------------------------------------- |
| Phase 0 | 1 สัปดาห์ | 4 | Infrastructure Ready + Security Base |
| Phase 1 | 2 สัปดาห์ | 5 | Auth & User Management + RBAC + Idempotency |
| Phase 2 | 1 สัปดาห์ | 5 | High-Integrity Data & File Management |
| Phase 3 | 2 สัปดาห์ | 4 | Unified Workflow Engine + Correspondence |
| Phase 4 | 2 สัปดาห์ | 3 | Drawing Management + RFA with Unified Workflow |
| Phase 5 | 2 สัปดาห์ | 2 | Workflow Systems + Resilience |
| Phase 6 | 1 สัปดาห์ | 5 | Notification & Resilience + Data Partitioning |
| Phase 7 | 3 สัปดาห์ | 9 | Testing & Hardening |
| Phase 8 | 1 สัปดาห์ | 6 | Documentation & Deploy |
| **รวม** | **15 สัปดาห์** | **39 Tasks** | **Production-Ready Backend v1.4.2** |
## **Document Control:**
- **Document:** Backend Development Plan v1.4.3
- **Version:** 1.4
- **Date:** 2025-11-22
- **Author:** NAP LCBP3-DMS & Gemini
- **Status:** FINAL
- **Classification:** Internal Technical Documentation
- **Approved By:** Nattanin
-----
`End of Backend Development Plan v1.4.3`

995
3_Frontend_Plan_V1_4_3.md Normal file
View File

@@ -0,0 +1,995 @@
# 📋 **แผนการพัฒนา Frontend (Next.js) - LCBP3-DMS v1.4.3**
**สถานะ:** FINAL GUIDELINE Rev.01
**วันที่:** 2025-11-22
**อ้างอิง:** Requirements v1.4.3 & FullStackJS Guidelines v1.4.3
**Classification:** Internal Technical Documentation
## 🎯 **ภาพรวมโครงการ**
พัฒนา Frontend สำหรับระบบบริหารจัดการเอกสารโครงการ (Document Management System) ที่มีความทันสมัย รองรับการทำงานบนอุปกรณ์ต่างๆ ได้อย่างสมบูรณ์ มีประสบการณ์ผู้ใช้ที่ราบรื่น และรองรับการทำงานแบบ Offline เบื้องต้น
---
## 📐 **สถาปัตยกรรมระบบ**
### **Technology Stack**
- **Framework:** Next.js 14+ (App Router, React 18, TypeScript, ESM)
- **Styling:** Tailwind CSS + PostCSS
- **UI Components:** shadcn/ui + Radix UI Primitives
- **State Management:**
- **Server State:** TanStack Query (React Query)
- **Client State:** Zustand
- **Form State:** React Hook Form + Zod
- **API Client:** Axios (พร้อม Idempotency Interceptor)
- **Authentication:** NextAuth.js (รองรับ JWT)
- **File Upload:** Custom Hook + Drag & Drop
- **Testing:**
- **Unit/Integration:** Vitest + React Testing Library
- **E2E:** Playwright
- **Mocking:** MSW (Mock Service Worker)
- **Development:**
- **Package Manager:** pnpm
- **Linting:** ESLint + Prettier
- **Type Checking:** TypeScript Strict Mode
### **โครงสร้างโปรเจกต์**
```tree
app/
├── (auth)/
│ ├── login/
│ └── register/
├── (dashboard)/
│ ├── layout.tsx
│ ├── page.tsx
│ └── components/
├── admin/
│ ├── users/
│ ├── roles/
│ └── numbering-formats/
├── correspondences/
│ ├── page.tsx
│ ├── [id]/
│ └── new/
├── rfas/
│ ├── page.tsx
│ ├── [id]/
│ └── new/
├── drawings/
├── circulations/
├── transmittals/
├── search/
└── profile/
components/
├── ui/ # shadcn/ui components
├── forms/ # Dynamic form components
├── tables/ # Responsive data tables
├── workflow/ # Workflow visualization
├── file-upload/ # File upload with security
├── notifications/ # Notification system
└── layout/ # App layout components
lib/
├── api/ # API clients & interceptors
├── auth/ # Authentication utilities
├── stores/ # Zustand stores
├── hooks/ # Custom React hooks
├── utils/ # Utility functions
├── constants/ # App constants
└── types/ # TypeScript type definitions
styles/
├── globals.css
└── components/
__tests__/
├── unit/
├── integration/
└── e2e/
```
---
## 🗓️ **แผนการพัฒนาแบบ Phase-Based**
### **Dependency Diagram (ภาพรวม)**
```mermaid
%% Phase 0: Foundation Setup
subgraph Phase0 [Phase 0: Foundation & Configuration]
F0_1[F0.1: Project Setup & Tooling]
F0_2[F0.2: Design System & UI Components]
F0_3[F0.3: API Client & Authentication]
F0_4[F0.4: State Management Setup]
end
%% Phase 1: Core Layout & Navigation
subgraph Phase1 [Phase 1: Core Application Structure]
F1_1[F1.1: Main Layout & Navigation]
F1_2[F1.2: Authentication Pages]
F1_3[F1.3: Dashboard & Landing]
F1_4[F1.4: Responsive Design System]
end
%% Phase 2: User Management & Profile
subgraph Phase2 [Phase 2: User Management & Security]
F2_1[F2.1: User Profile & Settings]
F2_2[F2.2: Admin Panel - User Management]
F2_3[F2.3: Admin Panel - Role Management]
F2_4[F2.4: Permission Integration]
end
%% Phase 3: Project & Organization Management
subgraph Phase3 [Phase 3: Project Structure]
F3_1[F3.1: Project Management UI]
F3_2[F3.2: Organization Management]
F3_3[F3.3: Contract Management]
end
%% Phase 4: Correspondence Management
subgraph Phase4 [Phase 4: Correspondence System]
F4_1[F4.1: Correspondence List & Search]
F4_2[F4.2: Correspondence Creation Form]
F4_3[F4.3: Correspondence Detail View]
F4_4[F4.4: File Upload Integration]
end
%% Phase 5: Workflow & Routing System
subgraph Phase5 [Phase 5: Workflow Management]
F5_1[F5.1: Workflow Visualization Component]
F5_2[F5.2: Routing Template Management]
F5_3[F5.3: Workflow Step Actions]
F5_4[F5.4: Real-time Status Updates]
end
%% Phase 6: Drawing Management
subgraph Phase6 [Phase 6: Drawing System]
F6_1[F6.1: Contract Drawings Management]
F6_2[F6.2: Shop Drawings Management]
F6_3[F6.3: Drawing Revision System]
F6_4[F6.4: Drawing References]
end
%% Phase 7: RFA & Approval Workflows
subgraph Phase7 [Phase 7: RFA System]
F7_1[F7.1: RFA List & Dashboard]
F7_2[F7.2: RFA Creation with Dynamic Forms]
F7_3[F7.3: RFA Workflow Integration]
F7_4[F7.4: RFA Approval Interface]
end
%% Phase 8: Circulation & Internal Routing
subgraph Phase8 [Phase 8: Internal Workflows]
F8_1[F8.1: Circulation Management]
F8_2[F8.2: Task Assignment Interface]
F8_3[F8.3: Internal Approval Flows]
end
%% Phase 9: Advanced Features
subgraph Phase9 [Phase 9: Advanced Features]
F9_1[F9.1: Advanced Search Interface]
F9_2[F9.2: Notification System]
F9_3[F9.3: Reporting & Analytics]
F9_4[F9.4: Mobile Optimization]
end
%% Phase 10: Testing & Optimization
subgraph Phase10 [Phase 10: Testing & Polish]
F10_1[F10.1: Comprehensive Testing]
F10_2[F10.2: Performance Optimization]
F10_3[F10.3: Security Hardening]
F10_4[F10.4: Documentation]
end
%% Dependencies
F0_1 --> F0_2
F0_1 --> F0_3
F0_1 --> F0_4
F0_2 --> F1_1
F0_3 --> F1_1
F0_4 --> F1_1
F1_1 --> F1_2
F1_1 --> F1_3
F1_1 --> F1_4
F1_1 --> F2_1
F1_3 --> F2_1
F0_3 --> F2_1
F2_1 --> F2_2
F2_2 --> F2_3
F2_3 --> F2_4
F1_1 --> F3_1
F2_4 --> F3_1
F3_1 --> F3_2
F3_2 --> F3_3
F1_1 --> F4_1
F3_1 --> F4_1
F4_1 --> F4_2
F4_2 --> F4_3
F4_2 --> F4_4
F4_1 --> F5_1
F4_2 --> F5_2
F4_3 --> F5_3
F5_1 --> F5_4
F3_1 --> F6_1
F4_4 --> F6_1
F6_1 --> F6_2
F6_2 --> F6_3
F6_3 --> F6_4
F4_1 --> F7_1
F5_1 --> F7_1
F6_2 --> F7_1
F7_1 --> F7_2
F7_2 --> F7_3
F7_3 --> F7_4
F4_1 --> F8_1
F5_3 --> F8_1
F8_1 --> F8_2
F8_2 --> F8_3
F4_1 --> F9_1
F7_1 --> F9_1
F1_3 --> F9_2
F5_4 --> F9_2
F1_3 --> F9_3
F1_4 --> F9_4
F1_1 --> F10_1
F4_1 --> F10_1
F7_1 --> F10_1
F10_1 --> F10_2
F10_2 --> F10_3
F10_3 --> F10_4
```
## **Phase 0: Foundation & Configuration (สัปดาห์ที่ 1)**
**Milestone:** โครงสร้างพื้นฐานพร้อม รองรับ Development Workflow ที่มีประสิทธิภาพ
### **Phase 0: Tasks**
- **[ ] F0.1 Project Setup & Tooling**
- [ ] Initialize Next.js 14+ project with TypeScript
- [ ] Configure pnpm workspace
- [ ] Setup ESLint, Prettier, and pre-commit hooks
- [ ] Configure Tailwind CSS with PostCSS
- [ ] Setup shadcn/ui components
- [ ] Configure absolute imports and path aliases
- [ ] **Deliverable:** Development environment ready
- [ ] **Dependencies:** None
- **[ ] F0.2 Design System & UI Components**
- [ ] Setup color palette and design tokens
- [ ] Create responsive design breakpoints
- [ ] Implement core shadcn/ui components:
- [ ] Button, Input, Label, Form
- [ ] Card, Table, Badge
- [ ] Dialog, Dropdown, Select
- [ ] Tabs, Accordion
- [ ] Create custom design system components:
- [ ] DataTable (responsive)
- [ ] FileUpload zone
- [ ] Workflow visualization
- [ ] **Deliverable:** Consistent UI component library
- [ ] **Dependencies:** F0.1
- **[ ] F0.3 API Client & Authentication**
- [ ] Setup Axios client with interceptors:
- [ ] Idempotency-Key header injection
- [ ] Authentication token management
- [ ] Error handling and retry logic
- [ ] Configure NextAuth.js for JWT authentication
- [ ] Create auth hooks (useAuth, usePermissions)
- [ ] Setup API route handlers for auth callbacks
- [ ] **Security:** Implement secure token storage
- [ ] **Deliverable:** Secure API communication layer
- [ ] **Dependencies:** F0.1
- **[ ] F0.4 State Management Setup**
- [ ] Configure TanStack Query for server state:
- [ ] Query client setup
- [ ] Default configurations
- [ ] Error boundaries
- [ ] Create Zustand stores:
- [ ] Auth store (user, permissions)
- [ ] UI store (theme, sidebar state)
- [ ] Draft store (form auto-save)
- [ ] Setup React Hook Form with Zod integration
- [ ] Create form validation schemas
- [ ] **Deliverable:** Robust state management system
- [ ] **Dependencies:** F0.1, F0.3
### **Phase 0: Testing - Foundation**
#### **F0.T1 Component Test Suite**
- [ ] **Unit Tests:** Core UI components (Button, Input, Card)
- [ ] **Integration Tests:** Form validation, API client interceptors
- [ ] **Visual Tests:** Component styling and responsive behavior
#### **F0.T2 Authentication Test Suite**
- [ ] **Unit Tests:** Auth hooks, token management
- [ ] **Integration Tests:** Login/logout flow, permission checks
- [ ] **Security Tests:** Token security, API authentication
---
## **Phase 1: Core Application Structure (สัปดาห์ที่ 2)**
**Milestone:** Layout หลักพร้อมใช้งาน การนำทางและ Authentication ทำงานได้
### **Phase 1: Tasks**
- **[ ] F1.1 Main Layout & Navigation**
- [ ] Create App Shell layout:
- [ ] Navbar with user menu and notifications
- [ ] Collapsible sidebar with navigation
- [ ] Main content area with responsive design
- [ ] Implement navigation menu structure:
- [ ] Dashboard, Correspondences, RFAs, Drawings, etc.
- [ ] Dynamic menu based on user permissions
- [ ] Create breadcrumb navigation component
- [ ] Implement mobile-responsive sidebar (drawer)
- [ ] Maintenance Mode Integration:
- [ ] Implement a Global Middleware/Wrapper ที่ตรวจสอบสถานะ Maintenance Mode ผ่าน API/Service ก่อนการ Render หน้า หากสถานะเป็น true ให้ Redirect ผู้ใช้ (ยกเว้น Admin) ไปยังหน้า /maintenance ทันที เพื่อให้สอดคล้องกับ Logic ของ Backend.
- [ ] **Accessibility:** Ensure keyboard navigation and screen reader support
- [ ] **Deliverable:** Fully functional application layout
- [ ] **Dependencies:** F0.2, F0.3
- **[ ] F1.2 Authentication Pages**
- [ ] Create login page with form validation
- [ ] Implement forgot password flow
- [ ] Create registration page (admin-only)
- [ ] Setup protected route middleware
- [ ] Implement route-based permission checks
- [ ] **Security:** Rate limiting on auth attempts, secure password requirements
- [ ] **Deliverable:** Complete authentication flow
- [ ] **Dependencies:** F0.3, F1.1
- **[ ] F1.3 Dashboard & Landing**
- [ ] Create public landing page for non-authenticated users
- [ ] Implement main dashboard with:
- [ ] KPI cards (document counts, pending tasks)
- [ ] "My Tasks" table from v_user_tasks
- [ ] Recent activity feed
- [ ] Security metrics display
- [ ] Create dashboard widgets system
- [ ] Implement data fetching with TanStack Query
- [ ] **Performance:** Optimize dashboard data loading
- [ ] **Deliverable:** Functional dashboard with real data
- [ ] **Dependencies:** F0.4, F1.1
- **[ ] F1.4 Responsive Design System**
- [ ] Implement mobile-first responsive design
- [ ] Create card view components for mobile tables
- [ ] Setup touch-friendly interactions
- [ ] Optimize images and assets for mobile
- [ ] Test across multiple device sizes
- [ ] **UX:** Ensure seamless mobile experience
- [ ] **Deliverable:** Fully responsive application
- [ ] **Dependencies:** F0.2, F1.1
### **Phase 1: Testing - Core Structure**
#### **F1.T1 Layout Test Suite**
- [ ] **Unit Tests:** Navigation components, layout responsiveness
- [ ] **Integration Tests:** Route protection, permission-based navigation
- [ ] **E2E Tests:** Complete user navigation flow
#### **F1.T2 Dashboard Test Suite**
- [ ] **Unit Tests:** Dashboard components, data formatting
- [ ] **Integration Tests:** Data fetching and display, real-time updates
- [ ] **Performance Tests:** Dashboard loading performance
---
## **Phase 2: User Management & Security (สัปดาห์ที่ 3)**
**Milestone:** การจัดการผู้ใช้และสิทธิ์แบบสมบูรณ์
### **Phase 2: Tasks**
- **[ ] F2.1 User Profile & Settings**
- [ ] Create user profile page:
- [ ] Personal information display/edit
- [ ] Password change functionality
- [ ] Notification preferences
- [ ] Implement profile picture upload
- [ ] Create user settings page
- [ ] **Security:** Secure password change with current password verification
- [ ] **Deliverable:** Complete user self-service management
- [ ] **Dependencies:** F1.1, F0.4
- **[ ] F2.2 Admin Panel - User Management**
- [ ] Create user list with search and filters
- [ ] Implement user creation form
- [ ] Create user edit interface
- [ ] Implement bulk user operations
- [ ] Add user activity tracking display
- [ ] **Security:** Admin-only access enforcement
- [ ] **Deliverable:** Comprehensive user management interface
- [ ] **Dependencies:** F1.1, F2.1
- **[ ] F2.3 Admin Panel - Role Management**
- [ ] Create role list and management interface
- [ ] Implement role creation and editing
- [ ] Create permission assignment interface
- [ ] Implement role-based access control visualization
- [ ] Add role usage statistics
- [ ] **Security:** Permission hierarchy enforcement
- [ ] **Deliverable:** Complete RBAC management system
- [ ] **Dependencies:** F2.2
- **[ ] F2.4 Permission Integration**
- [ ] Implement CASL ability integration
- [ ] Create permission-based UI components:
- [ ] ProtectedButton, ProtectedLink
- [ ] Conditional rendering based on permissions
- [ ] Setup real-time permission updates
- [ ] Implement permission debugging tools
- [ ] **Security:** Frontend-backend permission consistency
- [ ] **Deliverable:** Seamless permission enforcement throughout app
- [ ] **Dependencies:** F0.3, F2.3
### **Phase 2: Testing - User Management**
#### **F2.T1 User Management Test Suite**
- [ ] **Unit Tests:** User CRUD operations, form validation
- [ ] **Integration Tests:** User-role assignment, permission propagation
- [ ] **Security Tests:** Permission escalation attempts, admin access control
#### **F2.T2 RBAC Test Suite**
- [ ] **Unit Tests:** Permission checks, role validation
- [ ] **Integration Tests:** Multi-level permission enforcement, UI element protection
- [ ] **E2E Tests:** Complete role-based workflow testing
---
## **Phase 3: Project Structure (สัปดาห์ที่ 4)**
**Milestone:** การจัดการโครงสร้างโปรเจกต์และองค์กร
### **Phase 3: Tasks**
- **[ ] F3.1 Project Management UI**
- [ ] Create project list with search and filters
- [ ] Implement project creation and editing
- [ ] Create project detail view
- [ ] Implement project dashboard with statistics
- [ ] Add project member management
- [ ] **UX:** Intuitive project navigation and management
- [ ] **Deliverable:** Complete project management interface
- [ ] **Dependencies:** F1.1, F2.4
- **[ ] F3.2 Organization Management**
- [ ] Create organization list and management
- [ ] Implement organization creation and editing
- [ ] Create organization detail view
- [ ] Add organization user management
- [ ] Implement organization hierarchy visualization
- [ ] **Business Logic:** Proper organization-project relationships
- [ ] **Deliverable:** Comprehensive organization management
- [ ] **Dependencies:** F3.1
- **[ ] F3.3 Contract Management**
- [ ] Create contract list within projects
- [ ] Implement contract creation and editing
- [ ] Create contract detail view
- [ ] Add contract party management
- [ ] Implement contract document associations
- [ ] **Business Logic:** Contract-project-organization relationships
- [ ] **Deliverable:** Complete contract management system
- [ ] **Dependencies:** F3.1, F3.2
### **Phase 3: Testing - Project Structure**
#### **F3.T1 Project Management Test Suite**
- [ ] **Unit Tests:** Project CRUD operations, validation
- [ ] **Integration Tests:** Project-organization relationships, member management
- [ ] **Business Logic Tests:** Project hierarchy, access control
---
## **Phase 4: Correspondence System (สัปดาห์ที่ 5-6)**
**Milestone:** ระบบจัดการเอกสารโต้ตอบแบบสมบูรณ์
### **Phase 4: Tasks**
- **[ ] F4.1 Correspondence List & Search**
- [ ] Create correspondence list with advanced filtering:
- [ ] Filter by type, status, project, organization
- [ ] Search by title, document number, content
- [ ] Date range filtering
- [ ] Implement responsive data table:
- [ ] Desktop: Full table view
- [ ] Mobile: Card view conversion
- [ ] Add bulk operations (export, status change)
- [ ] Implement real-time updates
- [ ] **Performance:** Virtual scrolling for large datasets
- [ ] **Deliverable:** High-performance correspondence listing
- [ ] **Dependencies:** F1.1, F3.1
- **[ ] F4.2 Correspondence Creation Form**
- [ ] Create dynamic form generator based on JSON schema
- [ ] Implement form with multiple sections:
- [ ] Basic information (type, title, recipients)
- [ ] Content and details (JSON schema-based)
- [ ] File attachments
- [ ] Routing template selection
- [ ] [New] Implement "Originator Selector" component: Dropdown สำหรับเลือกองค์กรผู้ส่ง (แสดงเฉพาะเมื่อผู้ใช้มีสิทธิ์ system.manage_all หรือสิทธิ์พิเศษ) หากไม่เลือกให้ใช้ Organization ของผู้ใช้ตามปกติ
- [ ] Add draft auto-save functionality
- [ ] Implement form validation with Zod
- [ ] **UX:** Intuitive form with progress indication
- [ ] **Deliverable:** Flexible correspondence creation interface
- [ ] **Dependencies:** F0.4, F4.1
- **[ ] F4.3 Correspondence Detail View**
- [ ] Create comprehensive detail page:
- [ ] Document header with metadata
- [ ] Content display based on type
- [ ] Revision history
- [ ] Related documents
- [ ] Workflow status visualization
- [ ] Implement document actions:
- [ ] Edit, withdraw, cancel (with permissions)
- [ ] Download, print
- [ ] Create related documents
- [ ] Add comments and activity timeline
- [ ] **UX:** Clean, readable document presentation
- [ ] **Deliverable:** Complete document detail experience
- [ ] **Dependencies:** F4.1, F4.2
- **[ ] F4.4 File Upload Integration**
- [ ] Create drag-and-drop file upload component
- [ ] Implement file type validation and preview
- [ ] Add virus scan status indication
- [ ] Create file management interface:
- [ ] Mark files as main/supporting documents
- [ ] Reorder and manage attachments
- [ ] File security status display
- [ ] Implement two-phase upload progress
- [ ] **Security:** File type restrictions, size limits, virus scan integration
- [ ] **Deliverable:** Secure and user-friendly file management
- [ ] **Dependencies:** F0.2, F4.2
### **Phase 4: Testing - Correspondence System**
#### **F4.T1 Correspondence Test Suite**
- [ ] **Unit Tests:** Form validation, file upload components
- [ ] **Integration Tests:** Complete document lifecycle, file attachment flow
- [ ] **E2E Tests:** End-to-end correspondence creation and management
#### **F4.T2 File Upload Test Suite**
- [ ] **Unit Tests:** File validation, type checking
- [ ] **Integration Tests:** Two-phase upload process, virus scan integration
- [ ] **Security Tests:** Malicious file upload attempts, security feedback
---
## **Phase 5: Workflow Management (สัปดาห์ที่ 7)**
**Milestone:** ระบบ Visualization และจัดการ Workflow
### **Phase 5: Tasks**
- **[ ] F5.1 Workflow Visualization Component**
- [ ] Create horizontal workflow progress visualization
- [ ] Implement step status indicators (pending, active, completed, skipped)
- [ ] Add due date and assignee information
- [ ] Create interactive workflow diagram
- [ ] Implement workflow history timeline
- [ ] **UX:** Clear visual representation of complex workflows
- [ ] **Deliverable:** Intuitive workflow visualization
- [ ] **Dependencies:** F4.3
- **[ ] F5.2 Routing Template Management**
- [ ] Create routing template list and editor
- [ ] Implement drag-and-drop step configuration
- [ ] Add step configuration (purpose, duration, assignee rules)
- [ ] Create template preview functionality
- [ ] Implement template versioning
- [ ] **Business Logic:** Proper step sequencing and validation
- [ ] **Deliverable:** Comprehensive routing template management
- [ ] **Dependencies:** F3.1, F4.2
- **[ ] F5.3 Workflow Step Actions**
- [ ] Create step action interface:
- [ ] Approve, reject, request changes
- [ ] Add comments and attachments
- [ ] Forward to other users
- [ ] Implement bulk step actions
- [ ] Add action confirmation with reason required
- [ ] Create step delegation functionality
- [ ] **UX:** Streamlined step completion process
- [ ] **Deliverable:** Efficient workflow step management
- [ ] **Dependencies:** F5.1
- **[ ] F5.4 Real-time Status Updates**
- [ ] Implement WebSocket connections for real-time updates
- [ ] Create status change notifications
- [ ] Add auto-refresh for workflow states
- [ ] Implement optimistic updates for better UX
- [ ] Create update history and audit trail
- [ ] **Performance:** Efficient real-time data synchronization
- [ ] **Deliverable:** Real-time workflow monitoring
- [ ] **Dependencies:** F5.1, F9.2
### **Phase 5: Testing - Workflow Management**
#### **F5.T1 Workflow Test Suite**
- [ ] **Unit Tests:** Workflow visualization, step status logic
- [ ] **Integration Tests:** Complete workflow execution, real-time updates
- [ ] **E2E Tests:** Multi-step workflow with different user roles
---
## **Phase 6: Drawing System (สัปดาห์ที่ 8)**
**Milestone:** ระบบจัดการแบบแปลนแบบสมบูรณ์
### **Phase 6: Tasks**
- **[ ] F6.1 Contract Drawings Management**
- [ ] Create contract drawing list with categorization
- [ ] Implement drawing upload and metadata management
- [ ] Create drawing preview and viewer
- [ ] Add drawing version control
- [ ] Implement drawing search and filtering
- [ ] **UX:** Efficient drawing navigation and access
- [ ] **Deliverable:** Comprehensive contract drawing management
- [ ] **Dependencies:** F3.1, F4.4
- **[ ] F6.2 Shop Drawings Management**
- [ ] Create shop drawing list with revision tracking
- [ ] Implement shop drawing creation and revision system
- [ ] Create drawing comparison interface
- [ ] Add drawing approval status tracking
- [ ] Implement bulk drawing operations
- [ ] **Business Logic:** Proper revision control and approval workflows
- [ ] **Deliverable:** Complete shop drawing management system
- [ ] **Dependencies:** F6.1
- **[ ] F6.3 Drawing Revision System**
- [ ] Create revision history interface
- [ ] Implement revision comparison functionality
- [ ] Add revision notes and change tracking
- [ ] Create revision approval workflow
- [ ] Implement revision rollback capability
- [ ] **UX:** Clear visualization of changes between revisions
- [ ] **Deliverable:** Robust drawing revision control
- [ ] **Dependencies:** F6.2
- **[ ] F6.4 Drawing References**
- [ ] Create drawing reference management
- [ ] Implement cross-drawing references
- [ ] Add reference validation and integrity checks
- [ ] Create reference visualization
- [ ] Implement reference impact analysis
- [ ] **Business Logic:** Maintain reference integrity during changes
- [ ] **Deliverable:** Comprehensive drawing reference system
- [ ] **Dependencies:** F6.2, F6.3
### **Phase 6: Testing - Drawing System**
#### **F6.T1 Drawing Management Test Suite**
- [ ] **Unit Tests:** Drawing CRUD operations, revision logic
- [ ] **Integration Tests:** Drawing approval workflows, reference management
- [ ] **E2E Tests:** Complete drawing lifecycle with revisions
---
## **Phase 7: RFA System (สัปดาห์ที่ 9-10)**
**Milestone:** ระบบขออนุมัติแบบสมบูรณ์พร้อม Dynamic Forms
### **Phase 7: Tasks**
- **[ ] F7.1 RFA List & Dashboard**
- [ ] Create RFA dashboard with status overview
- [ ] Implement advanced RFA filtering and search
- [ ] Create RFA calendar view for deadlines
- [ ] Add RFA statistics and reporting
- [ ] Implement RFA bulk operations
- [ ] **UX:** Comprehensive RFA overview and management
- [ ] **Deliverable:** Complete RFA dashboard and listing
- [ ] **Dependencies:** F4.1, F5.1
- **[ ] F7.2 RFA Creation with Dynamic Forms**
- [ ] Create RFA type-specific form generator
- [ ] Implement dynamic form fields based on RFA type:
- [ ] RFA_DWG: Shop drawing selection
- [ ] RFA_DOC: Document specifications
- [ ] RFA_MES: Method statement details
- [ ] RFA_MAT: Material specifications
- [ ] Add form validation with JSON schema
- [ ] Implement form data persistence and recovery
- [ ] **UX:** Intuitive form experience for complex RFA types
- [ ] Dynamic Form & Schema Validation: สร้าง Component Dynamic Form Generator ที่:
- [ ] Fetch Schema: ดึงโครงสร้าง JSON Schema ที่ถูกต้องตาม rfa_type จาก Backend (ตาราง json_schemas ที่สร้างใหม่) ก่อนการ Render Form.
- [ ] Client-side Validation: Implement AJV (Another JSON Schema Validator) หรือไลบรารีที่เทียบเท่า เพื่อทำ Client-side Validation บนข้อมูล JSON ก่อนส่ง Submit.
- [ ] Implement dynamic form fields based on RFA type: RFA_DWG, RFA_DOC, RFA_MES, RFA_MAT.
- [ ] Add form data persistence and recovery.
- [ ] **Deliverable:** Flexible RFA creation system
- [ ] **Dependencies:** F4.2, F6.2
- **[ ] F7.3 RFA Workflow Integration**
- [ ] Integrate RFA with unified workflow engine
- [ ] Create RFA-specific workflow steps and actions
- [ ] Implement RFA approval interface
- [ ] Add RFA workflow history and tracking
- [ ] Create RFA workflow templates
- [ ] **Business Logic:** Proper RFA approval sequencing and validation
- [ ] **Deliverable:** Seamless RFA workflow integration
- [ ] **Dependencies:** F5.1, F7.2
- **[ ] F7.4 RFA Approval Interface**
- [ ] Create RFA review and approval interface
- [ ] Implement side-by-side document comparison
- [ ] Add approval comments and attachments
- [ ] Create conditional approval workflows
- [ ] Implement approval delegation and escalation
- [ ] **UX:** Efficient approval process for technical reviews
- [ ] **Deliverable:** Comprehensive RFA approval system
- [ ] **Dependencies:** F7.1, F7.3
### **Phase 7: Testing - RFA System**
#### **F7.T1 RFA Test Suite**
- [ ] **Unit Tests:** RFA form generation, validation logic
- [ ] **Integration Tests:** Complete RFA lifecycle, workflow integration
- [ ] **E2E Tests:** Multi-type RFA creation and approval workflows
---
## **Phase 8: Internal Workflows (สัปดาห์ที่ 11)**
**Milestone:** ระบบใบเวียนและการจัดการงานภายใน
### **Phase 8: Tasks**
- **[ ] F8.1 Circulation Management**
- [ ] Create circulation list and management interface
- [ ] Implement circulation creation from correspondence
- [ ] Create circulation template management
- [ ] Add circulation status tracking
- [ ] Implement circulation search and filtering
- [ ] **Business Logic:** Proper circulation-correspondence relationships
- [ ] **Deliverable:** Comprehensive circulation management
- [ ] **Dependencies:** F4.1, F5.2
- **[ ] F8.2 Task Assignment Interface**
- [ ] Create task assignment interface with user selection
- [ ] Implement task priority and deadline setting
- [ ] Add task dependency management
- [ ] Create task progress tracking
- [ ] Implement task reassignment and delegation
- [ ] **UX:** Intuitive task management and assignment
- [ ] **Deliverable:** Efficient task assignment system
- [ ] **Dependencies:** F8.1
- **[ ] F8.3 Internal Approval Flows**
- [ ] Create internal approval workflow interface
- [ ] Implement multi-level approval processes
- [ ] Add approval chain visualization
- [ ] Create approval delegation system
- [ ] Implement approval deadline management
- [ ] **Business Logic:** Proper approval hierarchy and escalation
- [ ] **Deliverable:** Robust internal approval system
- [ ] **Dependencies:** F8.1, F8.2
### **Phase 8: Testing - Internal Workflows**
#### **F8.T1 Circulation Test Suite**
- [ ] **Unit Tests:** Circulation creation, task assignment logic
- [ ] **Integration Tests:** Complete circulation workflow, internal approvals
- [ ] **E2E Tests:** End-to-end circulation with task completion
---
## **Phase 9: Advanced Features (สัปดาห์ที่ 12)**
**Milestone:** ฟีเจอร์ขั้นสูงและการปรับปรุงประสบการณ์ผู้ใช้
### **Phase 9: Tasks**
- **[ ] F9.1 Advanced Search Interface**
- [ ] Create unified search interface across all document types
- [ ] Implement faceted search with multiple filters
- [ ] Add search result highlighting and relevance scoring
- [ ] Create saved search and search templates
- [ ] Implement search result export functionality
- [ ] **Performance:** Efficient search with large datasets
- [ ] **Deliverable:** Powerful cross-document search system
- [ ] **Dependencies:** F4.1, F7.1
- **[ ] F9.2 Notification System**
- [ ] Create notification center with real-time updates
- [ ] Implement notification preferences management
- [ ] Add notification grouping and digest views
- [ ] Create actionable notifications with quick actions
- [ ] Implement notification read/unread status
- [ ] **UX:** Non-intrusive but effective notification delivery
- [ ] **Deliverable:** Comprehensive notification management
- [ ] **Dependencies:** F1.3, F5.4
- **[ ] F9.3 Reporting & Analytics**
- [ ] Create reporting dashboard with customizable widgets
- [ ] Implement data visualization components (charts, graphs)
- [ ] Add report scheduling and export
- [ ] Create ad-hoc reporting interface
- [ ] Implement performance metrics tracking
- [ ] **Business Logic:** Accurate data aggregation and reporting
- [ ] **Deliverable:** Powerful reporting and analytics system
- [ ] **Dependencies:** F1.3, F7.1
- **[ ] F9.4 Mobile Optimization**
- [ ] Implement touch-optimized interactions
- [ ] Create mobile-specific navigation patterns
- [ ] Add offline capability for critical functions
- [ ] Optimize images and assets for mobile networks
- [ ] Implement mobile-specific performance optimizations
- [ ] **UX:** Seamless mobile experience comparable to desktop
- [ ] **Deliverable:** Fully optimized mobile application
- [ ] **Dependencies:** F1.4
### **Phase 9: Testing - Advanced Features**
#### **F9.T1 Advanced Features Test Suite**
- [ ] **Unit Tests:** Search algorithms, notification logic
- [ ] **Integration Tests:** Cross-module search, real-time notifications
- [ ] **Performance Tests:** Search performance, mobile responsiveness
---
## **Phase 10: Testing & Polish (สัปดาห์ที่ 13-14)**
**Milestone:** แอปพลิเคชันที่ผ่านการทดสอบและปรับปรุงอย่างสมบูรณ์
### **Phase 10: Tasks**
- **[ ] F10.1 Comprehensive Testing**
- [ ] Idempotency Testing: เพิ่มการทดสอบเฉพาะสำหรับ Axios Interceptor เพื่อจำลองการส่ง Request POST/PUT/DELETE ที่มี Idempotency-Key ซ้ำไปยัง Mock API (MSW) เพื่อยืนยันว่า Client-side ไม่ส่ง Key ซ้ำในการทำงานปกติ และไม่เกิด Side Effect จากการ Replay Attack.
- [ ] Write unit tests for all components and utilities
- [ ] Create integration tests for critical user flows
- [ ] Implement E2E tests for complete workflows
- [ ] Perform cross-browser compatibility testing
- [ ] Conduct accessibility testing (WCAG 2.1 AA)
- [ ] **Quality:** 80%+ test coverage, all critical paths tested
- [ ] **Deliverable:** Fully tested application
- [ ] **Dependencies:** All previous phases
- **[ ] F10.2 Performance Optimization**
- [ ] Implement code splitting and lazy loading
- [ ] Optimize bundle size and asset delivery
- [ ] Add performance monitoring and metrics
- [ ] Implement caching strategies for static assets
- [ ] Optimize API call patterns and reduce over-fetching
- [ ] **Performance:** Core Web Vitals targets met
- [ ] **Deliverable:** High-performance application
- [ ] **Dependencies:** F10.1
- **[ ] F10.3 Security Hardening**
- [ ] Conduct security audit and penetration testing
- [ ] Implement Content Security Policy (CSP)
- [ ] Add security headers and protections
- [ ] Conduct dependency vulnerability scanning
- [ ] Implement secure coding practices review
- [ ] **Security:** No critical security vulnerabilities
- [ ] **Deliverable:** Security-hardened application
- [ ] **Dependencies:** F10.1
- **[ ] F10.4 Documentation**
- [ ] Create user documentation and guides
- [ ] Write technical documentation for developers
- [ ] Create API integration documentation
- [ ] Add inline code documentation
- [ ] Create deployment and maintenance guides
- [ ] **Quality:** Comprehensive and up-to-date documentation
- [ ] **Deliverable:** Complete documentation suite
- [ ] **Dependencies:** F10.1
### **Phase 10: Testing - Final Validation**
#### **F10.T1 Final Test Suite**
- [ ] **Performance Tests:** Load testing, stress testing
- [ ] **Security Tests:** Final security audit, vulnerability assessment
- [ ] **User Acceptance Tests:** Real user testing, feedback incorporation
- [ ] **Compatibility Tests:** Cross-browser, cross-device testing
---
## 📊 **สรุป Timeline**
| Phase | ระยะเวลา | จำนวนงาน | Output หลัก |
| -------- | ------------ | ------------ | ------------------------------------ |
| Phase 0 | 1 สัปดาห์ | 4 | Foundation & Tooling Ready |
| Phase 1 | 1 สัปดาห์ | 4 | Core Application Structure |
| Phase 2 | 1 สัปดาห์ | 4 | User Management & Security |
| Phase 3 | 1 สัปดาห์ | 3 | Project Structure Management |
| Phase 4 | 2 สัปดาห์ | 4 | Correspondence System |
| Phase 5 | 1 สัปดาห์ | 4 | Workflow Management |
| Phase 6 | 1 สัปดาห์ | 4 | Drawing System |
| Phase 7 | 2 สัปดาห์ | 4 | RFA System (Dynamic Forms) |
| Phase 8 | 1 สัปดาห์ | 3 | Internal Workflows |
| Phase 9 | 1 สัปดาห์ | 4 | Advanced Features |
| Phase 10 | 2 สัปดาห์ | 4 | Testing & Polish (Idempotency Test) |
| **รวม** | **14 สัปดาห์** | **39 Tasks** | **Production-Ready Frontend v1.4.2** |
---
## 🎯 **Critical Success Factors**
1. **User Experience First:** ทุกฟีเจอร์ต้องออกแบบเพื่อประสบการณ์ผู้ใช้ที่ดี
2. **Responsive Design:** รองรับการใช้งานบนอุปกรณ์ทุกรูปแบบ
3. **Performance:** Core Web Vitals ต้องอยู่ในเกณฑ์ที่ดี
4. **Accessibility:** ต้องเป็นไปตามมาตรฐาน WCAG 2.1 AA
5. **Security:** ป้องกัน XSS, CSRF และความเสี่ยงด้านความปลอดภัยอื่นๆ
6. **Offline Support:** รองรับการทำงานแบบ Offline เบื้องต้น
7. **Real-time Updates:** การอัปเดตสถานะแบบ Real-time
8. **Testing Coverage:** ครอบคลุมการทดสอบทุก Critical Path
9. **Documentation:** เอกสารครบถ้วนสำหรับผู้ใช้และนักพัฒนา
---
## 📋 **Quality Assurance Checklist**
### **ก่อน Production Deployment**
- [ ] **Performance:** Core Web Vitals ผ่านเกณฑ์
- [ ] **Accessibility:** WCAG 2.1 AA compliant
- [ ] **Security:** Security audit ผ่าน
- [ ] **Testing:** Test coverage ≥ 80%
- [ ] **Browser Compatibility:** ทำงานได้บนเบราว์เซอร์หลัก
- [ ] **Mobile Responsive:** ใช้งานได้ดีบนมือถือ
- [ ] **Documentation:** เอกสารครบถ้วน
- [ ] **User Acceptance:** ได้รับการยอมรับจากผู้ใช้
---
## 🚀 **ขั้นตอนถัดไป**
1. **Approve แผนนี้** → ปรับแต่งตาม Feedback
2. **Coordinate กับ Backend Team** → Sync API Specifications
3. **เริ่มพัฒนา Phase 0** → Setup Foundation
4. **Regular Sync** → ประสานงานกับ Backend ทุกสัปดาห์
5. **User Testing** → ทดสอบกับผู้ใช้จริงระหว่างพัฒนา
6. **Deploy to Production** → Week 15 (พร้อม Backend)
## **Document Control:**
- **Document:** Frontend Development Plan v1.4.3
- **Version:** 1.4
- **Date:** 2025-11-22
- **Author:** NAP LCBP3-DMS & Gemini
- **Status:** FINAL
- **Classification:** Internal Technical Documentation
- **Approved By:** Nattanin
---
`End of Frontend Development Plan v1.4.3 (ฉบับปรับปรุง)`

2840
4_Data_Dictionary_V1_4_3.md Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,7 @@
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.3",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^11.0.0",
"ajv": "^8.17.1",
@@ -48,6 +49,7 @@
"redlock": "5.0.0-beta.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.1",
"typeorm": "^0.3.27",
"uuid": "^13.0.0"
},

66
backend/pnpm-lock.yaml generated
View File

@@ -35,6 +35,9 @@ importers:
'@nestjs/platform-express':
specifier: ^11.0.1
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
'@nestjs/swagger':
specifier: ^11.2.3
version: 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
'@nestjs/throttler':
specifier: ^6.4.0
version: 6.4.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)
@@ -92,6 +95,9 @@ importers:
rxjs:
specifier: ^7.8.1
version: 7.8.2
swagger-ui-express:
specifier: ^5.0.1
version: 5.0.1(express@5.1.0)
typeorm:
specifier: ^0.3.27
version: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
@@ -756,6 +762,9 @@ packages:
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
engines: {node: '>=8'}
'@microsoft/tsdoc@0.16.0':
resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==}
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
cpu: [arm64]
@@ -887,6 +896,23 @@ packages:
peerDependencies:
typescript: '>=4.8.2'
'@nestjs/swagger@11.2.3':
resolution: {integrity: sha512-a0xFfjeqk69uHIUpP8u0ryn4cKuHdra2Ug96L858i0N200Hxho+n3j+TlQXyOF4EstLSGjTfxI1Xb2E1lUxeNg==}
peerDependencies:
'@fastify/static': ^8.0.0
'@nestjs/common': ^11.0.1
'@nestjs/core': ^11.0.1
class-transformer: '*'
class-validator: '*'
reflect-metadata: ^0.1.12 || ^0.2.0
peerDependenciesMeta:
'@fastify/static':
optional: true
class-transformer:
optional: true
class-validator:
optional: true
'@nestjs/testing@11.1.9':
resolution: {integrity: sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==}
peerDependencies:
@@ -948,6 +974,9 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@scarf/scarf@1.4.0':
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
'@sinclair/typebox@0.34.41':
resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
@@ -3226,6 +3255,15 @@ packages:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'}
swagger-ui-dist@5.30.2:
resolution: {integrity: sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==}
swagger-ui-express@5.0.1:
resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==}
engines: {node: '>= v0.10.32'}
peerDependencies:
express: '>=4.0.0 || >=5.0.0-beta'
symbol-observable@4.0.0:
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
engines: {node: '>=0.10'}
@@ -4330,6 +4368,8 @@ snapshots:
'@lukeed/csprng@1.1.0': {}
'@microsoft/tsdoc@0.16.0': {}
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
optional: true
@@ -4474,6 +4514,21 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)':
dependencies:
'@microsoft/tsdoc': 0.16.0
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
js-yaml: 4.1.1
lodash: 4.17.21
path-to-regexp: 8.3.0
reflect-metadata: 0.2.2
swagger-ui-dist: 5.30.2
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.14.2
'@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -4523,6 +4578,8 @@ snapshots:
'@pkgr/core@0.2.9': {}
'@scarf/scarf@1.4.0': {}
'@sinclair/typebox@0.34.41': {}
'@sinonjs/commons@3.0.1':
@@ -7067,6 +7124,15 @@ snapshots:
dependencies:
has-flag: 4.0.0
swagger-ui-dist@5.30.2:
dependencies:
'@scarf/scarf': 1.4.0
swagger-ui-express@5.0.1(express@5.1.0):
dependencies:
express: 5.1.0
swagger-ui-dist: 5.30.2
symbol-observable@4.0.0: {}
synckit@0.11.11:

View File

@@ -0,0 +1,52 @@
import { DataSource } from 'typeorm';
import * as fs from 'fs';
// Read .env to get DB config
const envFile = fs.readFileSync('.env', 'utf8');
const getEnv = (key: string) => {
const line = envFile.split('\n').find(l => l.startsWith(key + '='));
return line ? line.split('=')[1].trim() : '';
};
const dataSource = new DataSource({
type: 'mariadb',
host: getEnv('DB_HOST') || 'localhost',
port: parseInt(getEnv('DB_PORT') || '3306'),
username: getEnv('DB_USERNAME') || 'admin',
password: getEnv('DB_PASSWORD') || 'Center2025',
database: getEnv('DB_DATABASE') || 'lcbp3_dev',
entities: [],
synchronize: false,
});
async function main() {
await dataSource.initialize();
console.log('Connected to DB');
try {
const assignments = await dataSource.query('SELECT * FROM user_assignments');
console.log('All Assignments:', assignments);
// Check if User 3 has any assignment
const user3Assign = assignments.find((a: any) => a.user_id === 3);
if (!user3Assign) {
console.log('User 3 has NO assignments.');
// Try to insert assignment for User 3 (Editor)
console.log('Inserting assignment for User 3 (Role 4, Org 41)...');
await dataSource.query(`
INSERT INTO user_assignments (user_id, role_id, organization_id, assigned_by_user_id)
VALUES (3, 4, 41, 1)
`);
console.log('Inserted assignment for User 3.');
} else {
console.log('User 3 Assignment:', user3Assign);
}
} catch (err) {
console.error(err);
} finally {
await dataSource.destroy();
}
}
main();

View File

@@ -0,0 +1,126 @@
import * as crypto from 'crypto';
// Configuration
const JWT_SECRET =
'eebc122aa65adde8c76c6a0847d9649b2b67a06db1504693e6c912e51499b76e';
const API_URL = 'http://localhost:3000/api';
// Helper to sign JWT
function signJwt(payload: any) {
const header = { alg: 'HS256', typ: 'JWT' };
const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
'base64url',
);
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString(
'base64url',
);
const signature = crypto
.createHmac('sha256', JWT_SECRET)
.update(encodedHeader + '.' + encodedPayload)
.digest('base64url');
return `${encodedHeader}.${encodedPayload}.${signature}`;
}
async function main() {
// 1. Generate Token for Editor01 (ID 3)
const token = signJwt({ username: 'editor01', sub: 3 });
console.log('Generated Token:', token);
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
};
try {
// 1.5 Check Permissions
console.log('\nChecking Permissions...');
const permRes = await fetch(`${API_URL}/users/me/permissions`, { headers });
if (permRes.ok) {
const perms = await permRes.json();
console.log('My Permissions:', perms);
} else {
console.error(
'Failed to get permissions:',
permRes.status,
await permRes.text(),
);
}
// 2. Create Correspondence
console.log('\nCreating Correspondence...');
const createRes = await fetch(`${API_URL}/correspondences`, {
method: 'POST',
headers,
body: JSON.stringify({
projectId: 1,
typeId: 1, // Assuming ID 1 exists (e.g., RFA or Memo)
// originatorId: 1, // Removed for Admin user
title: 'Manual Verification Doc',
details: { note: 'Created via script' },
}),
});
if (!createRes.ok) {
throw new Error(
`Create failed: ${createRes.status} ${await createRes.text()}`,
);
}
const doc: any = await createRes.json();
console.log('Created Document:', doc.id, doc.correspondenceNumber);
// 3. Submit Workflow
console.log('\nSubmitting Workflow...');
const submitRes = await fetch(
`${API_URL}/correspondences/${doc.id}/submit`,
{
method: 'POST',
headers,
body: JSON.stringify({
templateId: 1, // Assuming Template ID 1 exists
}),
},
);
if (!submitRes.ok) {
const text = await submitRes.text();
console.error(`Submit failed: ${submitRes.status} ${text}`);
if (text.includes('template')) {
console.warn(
'⚠️ Template ID 1 not found. Please ensure a Routing Template exists.',
);
}
return;
}
console.log('Workflow Submitted Successfully');
// 4. Approve Workflow (as same user for simplicity, assuming logic allows or user has permission)
console.log('\nApproving Workflow...');
const approveRes = await fetch(
`${API_URL}/correspondences/${doc.id}/workflow/action`,
{
method: 'POST',
headers,
body: JSON.stringify({
action: 'APPROVE',
comment: 'Approved via script',
}),
},
);
if (!approveRes.ok) {
throw new Error(
`Approve failed: ${approveRes.status} ${await approveRes.text()}`,
);
}
console.log('Workflow Approved Successfully');
} catch (error: any) {
console.error('Error:', error.message);
}
}
main().catch((err) => console.error(err));

View File

@@ -11,7 +11,7 @@ import { envValidationSchema } from './common/config/env.validation.js'; // ส
// import { CommonModule } from './common/common.module';
import { UserModule } from './modules/user/user.module';
import { ProjectModule } from './modules/project/project.module';
import { FileStorageModule } from './modules/file-storage/file-storage.module';
import { FileStorageModule } from './common/file-storage/file-storage.module.js';
import { DocumentNumberingModule } from './modules/document-numbering/document-numbering.module';
import { AuthModule } from './common/auth/auth.module.js'; // <--- เพิ่ม Import นี้ T2.4
import { JsonSchemaModule } from './modules/json-schema/json-schema.module.js';

View File

@@ -5,7 +5,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service.js';
import { AuthController } from './auth.controller.js';
import { UserModule } from '../../modules/user/user.module.js';
import { JwtStrategy } from './jwt.strategy.js';
import { JwtStrategy } from '../guards/jwt.strategy.js';
@Module({
imports: [

View File

@@ -0,0 +1,19 @@
// File: src/common/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
/**
* Decorator สำหรับดึงข้อมูล User ปัจจุบันจาก Request Object
* ใช้คู่กับ JwtAuthGuard
*
* ตัวอย่างการใช้:
* @Get()
* findAll(@CurrentUser() user: User) { ... }
*/
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
// request.user ถูก set โดย Passport/JwtStrategy
return request.user;
},
);

View File

@@ -6,7 +6,7 @@ import {
ManyToOne,
JoinColumn,
} from 'typeorm';
import { User } from '../../user/entities/user.entity.js';
import { User } from '../../../modules/user/entities/user.entity.js';
@Entity('attachments')
export class Attachment {

View File

@@ -11,7 +11,7 @@ import {
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { FileStorageService } from './file-storage.service.js';
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
import { JwtAuthGuard } from '../guards/jwt-auth.guard.js';
// ✅ 1. สร้าง Interface เพื่อระบุ Type ของ Request
interface RequestWithUser {

View File

@@ -9,18 +9,26 @@ interface JwtPayload {
username: string;
}
import { UserService } from '../../modules/user/user.service.js';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
constructor(
configService: ConfigService,
private userService: UserService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
// ใส่ ! เพื่อยืนยันว่ามีค่าแน่นอน (ConfigValidation เช็คให้แล้ว)
secretOrKey: configService.get<string>('JWT_SECRET')!,
});
}
async validate(payload: JwtPayload) {
return { userId: payload.sub, username: payload.username };
const user = await this.userService.findOne(payload.sub);
if (!user) {
throw new Error('User not found');
}
return user;
}
}

View File

@@ -12,11 +12,15 @@ import { CorrespondenceService } from './correspondence.service.js';
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto.js';
import { SubmitCorrespondenceDto } from './dto/submit-correspondence.dto.js'; // <--- ✅ 3. เพิ่ม Import DTO นี้
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
import { RbacGuard } from '../../common/auth/rbac.guard.js';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RbacGuard } from '../../common/guards/rbac.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
import { WorkflowActionDto } from './dto/workflow-action.dto.js';
// ... imports ...
import { AddReferenceDto } from './dto/add-reference.dto.js';
import { SearchCorrespondenceDto } from './dto/search-correspondence.dto.js';
import { Query, Delete } from '@nestjs/common'; // เพิ่ม Query, Delete
@Controller('correspondences')
@UseGuards(JwtAuthGuard, RbacGuard)
export class CorrespondenceController {
@@ -38,10 +42,11 @@ export class CorrespondenceController {
return this.correspondenceService.create(createDto, req.user);
}
// ✅ ปรับปรุง findAll ให้รับ Query Params
@Get()
@RequirePermission('document.view') // 🔒 ต้องมีสิทธิ์ดู
findAll() {
return this.correspondenceService.findAll();
@RequirePermission('document.view')
findAll(@Query() searchDto: SearchCorrespondenceDto) {
return this.correspondenceService.findAll(searchDto);
}
// ✅ เพิ่ม Endpoint นี้ครับ
@@ -58,4 +63,30 @@ export class CorrespondenceController {
req.user,
);
}
// --- REFERENCES ---
@Get(':id/references')
@RequirePermission('document.view')
getReferences(@Param('id', ParseIntPipe) id: number) {
return this.correspondenceService.getReferences(id);
}
@Post(':id/references')
@RequirePermission('document.edit') // ต้องมีสิทธิ์แก้ไขถึงจะเพิ่ม Ref ได้
addReference(
@Param('id', ParseIntPipe) id: number,
@Body() dto: AddReferenceDto,
) {
return this.correspondenceService.addReference(id, dto);
}
@Delete(':id/references/:targetId')
@RequirePermission('document.edit')
removeReference(
@Param('id', ParseIntPipe) id: number,
@Param('targetId', ParseIntPipe) targetId: number,
) {
return this.correspondenceService.removeReference(id, targetId);
}
}

View File

@@ -15,6 +15,8 @@ import { DocumentNumberingModule } from '../document-numbering/document-numberin
import { JsonSchemaModule } from '../json-schema/json-schema.module.js'; // ต้องใช้ Validate Details
import { UserModule } from '../user/user.module.js'; // <--- 1. Import UserModule
import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.js'; // <--- ✅ เพิ่มบรรทัดนี้ครับ
import { CorrespondenceReference } from './entities/correspondence-reference.entity.js';
@Module({
imports: [
TypeOrmModule.forFeature([
@@ -25,6 +27,7 @@ import { WorkflowEngineModule } from '../workflow-engine/workflow-engine.module.
RoutingTemplate, // <--- ลงทะเบียน
RoutingTemplateStep, // <--- ลงทะเบียน
CorrespondenceRouting, // <--- ลงทะเบียน
CorrespondenceReference, // <--- ลงทะเบียน
]),
DocumentNumberingModule, // Import เพื่อขอเลขที่เอกสาร
JsonSchemaModule, // Import เพื่อ Validate JSON

View File

@@ -1,11 +1,15 @@
// File: src/modules/correspondence/correspondence.service.ts
import {
Injectable,
NotFoundException,
BadRequestException,
InternalServerErrorException,
ForbiddenException,
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { Repository, DataSource, Like, In } from 'typeorm';
// Entities
import { Correspondence } from './entities/correspondence.entity.js';
@@ -14,22 +18,28 @@ 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'; // Entity สำหรับตารางเชื่อมโยง
import { User } from '../user/entities/user.entity.js';
// 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';
// Interfaces
// Interfaces & Enums
import { WorkflowAction } from '../workflow-engine/interfaces/workflow.interface.js';
// 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';
@Injectable()
export class CorrespondenceService {
private readonly logger = new Logger(CorrespondenceService.name);
constructor(
@InjectRepository(Correspondence)
private correspondenceRepo: Repository<Correspondence>,
@@ -43,16 +53,26 @@ export class CorrespondenceService {
private templateRepo: Repository<RoutingTemplate>,
@InjectRepository(CorrespondenceRouting)
private routingRepo: Repository<CorrespondenceRouting>,
@InjectRepository(CorrespondenceReference)
private referenceRepo: Repository<CorrespondenceReference>,
private numberingService: DocumentNumberingService,
private jsonSchemaService: JsonSchemaService,
private workflowEngine: WorkflowEngineService,
private userService: UserService,
private dataSource: DataSource,
) {}
// --- 1. CREATE DOCUMENT ---
/**
* สร้างเอกสารใหม่ (Create Document)
* รองรับ Impersonation Logic: Superadmin สามารถสร้างในนามองค์กรอื่นได้
*
* @param createDto ข้อมูลสำหรับการสร้างเอกสาร
* @param user ผู้ใช้งานที่ทำการสร้าง
* @returns ข้อมูลเอกสารที่สร้างเสร็จแล้ว
*/
async create(createDto: CreateCorrespondenceDto, user: User) {
// 1.1 Validate Basic Info
// 1. ตรวจสอบข้อมูลพื้นฐาน (Basic Validation)
const type = await this.typeRepo.findOne({
where: { id: createDto.typeId },
});
@@ -62,59 +82,101 @@ export class CorrespondenceService {
where: { statusCode: 'DRAFT' },
});
if (!statusDraft) {
throw new InternalServerErrorException('Status DRAFT not found');
throw new InternalServerErrorException(
'Status DRAFT not found in Master Data',
);
}
const userOrgId = user.primaryOrganizationId;
// 2. Impersonation Logic & Organization Context
// กำหนด Org เริ่มต้นเป็นของผู้ใช้งานปัจจุบัน
let userOrgId = user.primaryOrganizationId;
// Fallback: หากใน Token ไม่มี Org ID ให้ดึงจาก DB อีกครั้งเพื่อความชัวร์
if (!userOrgId) {
throw new BadRequestException('User must belong to an organization');
const fullUser = await this.userService.findOne(user.user_id);
if (fullUser) {
userOrgId = fullUser.primaryOrganizationId;
}
}
// 1.2 Validate JSON Details
// ตรวจสอบกรณีต้องการสร้างในนามองค์กรอื่น (Impersonation)
if (createDto.originatorId && createDto.originatorId !== userOrgId) {
// ดึง Permissions ของผู้ใช้มาตรวจสอบ
const permissions = await this.userService.getUserPermissions(
user.user_id,
);
// ผู้ใช้ต้องมีสิทธิ์ 'system.manage_all' เท่านั้นจึงจะสวมสิทธิ์ได้
if (!permissions.includes('system.manage_all')) {
throw new ForbiddenException(
'You do not have permission to create documents on behalf of other organizations.',
);
}
// อนุญาตให้ใช้ Org ID ที่ส่งมา
userOrgId = createDto.originatorId;
}
// Final Validation: ต้องมี Org ID เสมอ
if (!userOrgId) {
throw new BadRequestException(
'User must belong to an organization to create documents',
);
}
// 3. Validate JSON Details (ถ้ามี)
if (createDto.details) {
try {
await this.jsonSchemaService.validate(type.typeCode, createDto.details);
} catch (error: any) {
console.warn(`Schema validation warning: ${error.message}`);
// Log warning แต่ไม่ Block การสร้าง (ตามความยืดหยุ่นที่ต้องการ) หรือจะ Throw ก็ได้ตาม Req
this.logger.warn(
`Schema validation warning for ${type.typeCode}: ${error.message}`,
);
}
}
// 4. เริ่ม Transaction (เพื่อความสมบูรณ์ของข้อมูล: เลขที่เอกสาร + ตัวเอกสาร + Revision)
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1.3 Generate Document Number (Double-Lock)
// 4.1 ขอเลขที่เอกสาร (Double-Lock Mechanism ผ่าน NumberingService)
// TODO: Fetch ORG_CODE จาก DB จริงๆ โดยใช้ userOrgId
const orgCode = 'ORG'; // Mock ไว้ก่อน ควร query จาก Organization Entity
const docNumber = await this.numberingService.generateNextNumber(
createDto.projectId,
userOrgId,
userOrgId, // ใช้ ID ของเจ้าของเอกสารจริง (Originator)
createDto.typeId,
new Date().getFullYear(),
{
TYPE_CODE: type.typeCode,
ORG_CODE: 'ORG', // In real app, fetch user's org code
ORG_CODE: orgCode,
},
);
// 1.4 Save Head
// 4.2 สร้าง Correspondence (หัวจดหมาย)
const correspondence = queryRunner.manager.create(Correspondence, {
correspondenceNumber: docNumber,
correspondenceTypeId: createDto.typeId,
projectId: createDto.projectId,
originatorId: userOrgId,
originatorId: userOrgId, // บันทึก Org ที่ถูกต้อง
isInternal: createDto.isInternal || false,
createdBy: user.user_id,
});
const savedCorr = await queryRunner.manager.save(correspondence);
// 1.5 Save First Revision
// 4.3 สร้าง Revision แรก (Rev 0)
const revision = queryRunner.manager.create(CorrespondenceRevision, {
correspondenceId: savedCorr.id,
revisionNumber: 0,
revisionLabel: 'A',
revisionLabel: 'A', // หรือเริ่มที่ 0 แล้วแต่ Business Logic
isCurrent: true,
statusId: statusDraft.id,
title: createDto.title,
description: createDto.description, // ถ้ามีใน DTO
details: createDto.details,
createdBy: user.user_id,
});
@@ -128,24 +190,70 @@ export class CorrespondenceService {
};
} catch (err) {
await queryRunner.rollbackTransaction();
this.logger.error(
`Failed to create correspondence: ${(err as Error).message}`,
);
throw err;
} finally {
await queryRunner.release();
}
}
// --- READ ---
async findAll() {
return this.correspondenceRepo.find({
relations: ['revisions', 'type', 'project'],
order: { createdAt: 'DESC' },
});
/**
* ค้นหาเอกสาร (Find All)
* รองรับการกรองและค้นหา
*/
async findAll(searchDto: SearchCorrespondenceDto = {}) {
const { search, typeId, projectId, statusId } = searchDto;
const query = this.correspondenceRepo
.createQueryBuilder('corr')
.leftJoinAndSelect('corr.revisions', 'rev')
.leftJoinAndSelect('corr.type', 'type')
.leftJoinAndSelect('corr.project', 'project')
.leftJoinAndSelect('corr.originator', 'org')
.where('rev.isCurrent = :isCurrent', { isCurrent: true }); // ดูเฉพาะ Rev ปัจจุบัน
if (projectId) {
query.andWhere('corr.projectId = :projectId', { projectId });
}
if (typeId) {
query.andWhere('corr.correspondenceTypeId = :typeId', { typeId });
}
if (statusId) {
query.andWhere('rev.statusId = :statusId', { statusId });
}
if (search) {
query.andWhere(
'(corr.correspondenceNumber LIKE :search OR rev.title LIKE :search)',
{ search: `%${search}%` },
);
}
query.orderBy('corr.createdAt', 'DESC');
return query.getMany();
}
/**
* ดึงข้อมูลเอกสารรายตัว (Find One)
* พร้อม Relations ที่จำเป็น
*/
async findOne(id: number) {
const correspondence = await this.correspondenceRepo.findOne({
where: { id },
relations: ['revisions', 'type', 'project'],
relations: [
'revisions',
'revisions.status', // สถานะของ Revision
'type',
'project',
'originator',
// 'tags', // ถ้ามี Relation
// 'attachments' // ถ้ามี Relation ผ่าน Junction
],
});
if (!correspondence) {
@@ -154,9 +262,11 @@ export class CorrespondenceService {
return correspondence;
}
// --- 2. SUBMIT WORKFLOW ---
/**
* ส่งเอกสารเข้า Workflow (Submit)
* สร้าง Routing เริ่มต้นตาม Template
*/
async submit(correspondenceId: number, templateId: number, user: User) {
// 2.1 Get Document & Current Revision
const correspondence = await this.correspondenceRepo.findOne({
where: { id: correspondenceId },
relations: ['revisions'],
@@ -171,7 +281,9 @@ export class CorrespondenceService {
throw new NotFoundException('Current revision not found');
}
// 2.2 Get Template Config
// ตรวจสอบสถานะปัจจุบัน (ต้องเป็น DRAFT หรือสถานะที่แก้ได้)
// TODO: เพิ่ม Logic ตรวจสอบ Status ID ว่าเป็น DRAFT หรือไม่
const template = await this.templateRepo.findOne({
where: { id: templateId },
relations: ['steps'],
@@ -179,7 +291,9 @@ export class CorrespondenceService {
});
if (!template || !template.steps?.length) {
throw new BadRequestException('Invalid routing template');
throw new BadRequestException(
'Invalid routing template or no steps defined',
);
}
const queryRunner = this.dataSource.createQueryRunner();
@@ -189,23 +303,25 @@ export class CorrespondenceService {
try {
const firstStep = template.steps[0];
// 2.3 Create First Routing Record
// สร้าง Routing Record แรก
const routing = queryRunner.manager.create(CorrespondenceRouting, {
correspondenceId: currentRevision.id,
templateId: template.id, // ✅ Save templateId for reference
correspondenceId: currentRevision.id, // ผูกกับ Revision
templateId: template.id, // บันทึก templateId ไว้ใช้อ้างอิง
sequence: 1,
fromOrganizationId: user.primaryOrganizationId,
toOrganizationId: firstStep.toOrganizationId,
fromOrganizationId: user.primaryOrganizationId, // ส่งจากเรา
toOrganizationId: firstStep.toOrganizationId, // ไปยังผู้รับคนแรก
stepPurpose: firstStep.stepPurpose,
status: 'SENT',
dueDate: new Date(
Date.now() + (firstStep.expectedDays || 7) * 24 * 60 * 60 * 1000,
),
processedByUserId: user.user_id,
processedByUserId: user.user_id, // บันทึกว่าใครกดส่ง
processedAt: new Date(),
});
await queryRunner.manager.save(routing);
// TODO: อัปเดตสถานะเอกสารเป็น SUBMITTED (เปลี่ยน statusId ใน Revision)
await queryRunner.commitTransaction();
return routing;
} catch (err) {
@@ -216,14 +332,15 @@ export class CorrespondenceService {
}
}
// --- 3. PROCESS ACTION (Approve/Reject/Return) ---
/**
* ประมวลผล Action ใน Workflow (Approve/Reject/Etc.)
*/
async processAction(
correspondenceId: number,
dto: WorkflowActionDto,
user: User,
) {
// 3.1 Find Active Routing Step
// Find correspondence first to ensure it exists
// 1. Find Document & Current Revision
const correspondence = await this.correspondenceRepo.findOne({
where: { id: correspondenceId },
relations: ['revisions'],
@@ -236,35 +353,33 @@ export class CorrespondenceService {
if (!currentRevision)
throw new NotFoundException('Current revision not found');
// Find the latest routing step
// 2. Find Active Routing Step (Status = SENT)
// หาสเต็ปล่าสุดที่ส่งมาถึง Org ของเรา และสถานะเป็น SENT
const currentRouting = await this.routingRepo.findOne({
where: {
correspondenceId: currentRevision.id,
// In real scenario, we might check status 'SENT' or 'RECEIVED'
status: 'SENT',
},
order: { sequence: 'DESC' },
relations: ['toOrganization'],
});
if (
!currentRouting ||
currentRouting.status === 'ACTIONED' ||
currentRouting.status === 'REJECTED'
) {
if (!currentRouting) {
throw new BadRequestException(
'No active workflow step found or step already processed',
'No active workflow step found for this document',
);
}
// 3.2 Check Permissions
// User must belong to the target organization of the current step
// 3. Check Permissions (Must be in target Org)
// Logic: ผู้กด Action ต้องสังกัด Org ที่เป็นปลายทางของ Routing นี้
// TODO: เพิ่ม Logic ให้ Superadmin หรือ Document Control กดแทนได้
if (currentRouting.toOrganizationId !== user.primaryOrganizationId) {
throw new BadRequestException(
'You are not authorized to process this step',
);
}
// 3.3 Load Template to find Next Step Config
// 4. Load Template to find Next Step Config
if (!currentRouting.templateId) {
throw new InternalServerErrorException(
'Routing record missing templateId',
@@ -283,7 +398,7 @@ export class CorrespondenceService {
const totalSteps = template.steps.length;
const currentSeq = currentRouting.sequence;
// 3.4 Calculate Next State using Workflow Engine
// 5. Calculate Next State using Workflow Engine Service
const result = this.workflowEngine.processAction(
currentSeq,
totalSteps,
@@ -291,12 +406,13 @@ export class CorrespondenceService {
dto.returnToSequence,
);
// 6. Execute Database Updates
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 3.5 Update Current Step
// 6.1 Update Current Step
currentRouting.status =
dto.action === WorkflowAction.REJECT ? 'REJECTED' : 'ACTIONED';
currentRouting.processedByUserId = user.user_id;
@@ -305,39 +421,43 @@ export class CorrespondenceService {
await queryRunner.manager.save(currentRouting);
// 3.6 Create Next Step (If exists and not rejected)
// 6.2 Create Next Step (If exists and not rejected/completed)
if (result.nextStepSequence && dto.action !== WorkflowAction.REJECT) {
// ✅ Find config for next step from Template
// ค้นหา Config ของ Step ถัดไปจาก Template
const nextStepConfig = template.steps.find(
(s) => s.sequence === result.nextStepSequence,
);
if (!nextStepConfig) {
throw new InternalServerErrorException(
`Configuration for step ${result.nextStepSequence} not found`,
// อาจจะเป็นกรณี End of Workflow หรือ Logic Error
this.logger.warn(
`Next step ${result.nextStepSequence} not found in template`,
);
} else {
const nextRouting = queryRunner.manager.create(
CorrespondenceRouting,
{
correspondenceId: currentRevision.id,
templateId: template.id,
sequence: result.nextStepSequence,
fromOrganizationId: user.primaryOrganizationId, // ส่งจากคนปัจจุบัน
toOrganizationId: nextStepConfig.toOrganizationId, // ไปยังคนถัดไปตาม Template
stepPurpose: nextStepConfig.stepPurpose,
status: 'SENT',
dueDate: new Date(
Date.now() +
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000,
),
},
);
await queryRunner.manager.save(nextRouting);
}
const nextRouting = queryRunner.manager.create(CorrespondenceRouting, {
correspondenceId: currentRevision.id,
templateId: template.id,
sequence: result.nextStepSequence,
fromOrganizationId: user.primaryOrganizationId, // Forwarded by current user
toOrganizationId: nextStepConfig.toOrganizationId, // ✅ Real Target from Template
stepPurpose: nextStepConfig.stepPurpose, // ✅ Real Purpose from Template
status: 'SENT',
dueDate: new Date(
Date.now() +
(nextStepConfig.expectedDays || 7) * 24 * 60 * 60 * 1000,
),
});
await queryRunner.manager.save(nextRouting);
}
// 3.7 Update Document Status (Optional - if Engine suggests)
// 6.3 Update Document Status (Optional / Based on result)
if (result.shouldUpdateStatus) {
// Example: Update revision status to APPROVED or REJECTED
// await this.updateDocumentStatus(currentRevision, result.documentStatus);
// Logic เปลี่ยนสถานะ revision เช่นจาก SUBMITTED -> APPROVED
// await this.updateDocumentStatus(currentRevision, result.documentStatus, queryRunner);
}
await queryRunner.commitTransaction();
@@ -349,4 +469,81 @@ export class CorrespondenceService {
await queryRunner.release();
}
}
// --- REFERENCE MANAGEMENT ---
/**
* เพิ่มเอกสารอ้างอิง (Add Reference)
* ตรวจสอบ Circular Reference และ Duplicate
*/
async addReference(id: number, dto: AddReferenceDto) {
// 1. เช็คว่าเอกสารทั้งคู่มีอยู่จริง
const source = await this.correspondenceRepo.findOne({ where: { id } });
const target = await this.correspondenceRepo.findOne({
where: { id: dto.targetId },
});
if (!source || !target) {
throw new NotFoundException('Source or Target correspondence not found');
}
// 2. ป้องกันการอ้างอิงตัวเอง (Self-Reference)
if (source.id === target.id) {
throw new BadRequestException('Cannot reference self');
}
// 3. ตรวจสอบว่ามีอยู่แล้วหรือไม่ (Duplicate Check)
const exists = await this.referenceRepo.findOne({
where: {
sourceId: id,
targetId: dto.targetId,
},
});
if (exists) {
return exists; // ถ้ามีแล้วก็คืนตัวเดิมไป (Idempotency)
}
// 4. สร้าง Reference
const ref = this.referenceRepo.create({
sourceId: id,
targetId: dto.targetId,
});
return this.referenceRepo.save(ref);
}
/**
* ลบเอกสารอ้างอิง (Remove Reference)
*/
async removeReference(id: number, targetId: number) {
const result = await this.referenceRepo.delete({
sourceId: id,
targetId: targetId,
});
if (result.affected === 0) {
throw new NotFoundException('Reference not found');
}
}
/**
* ดึงรายการเอกสารอ้างอิง (Get References)
* ทั้งที่อ้างถึง (Outgoing) และถูกอ้างถึง (Incoming)
*/
async getReferences(id: number) {
// ดึงรายการที่เอกสารนี้ไปอ้างถึง (Outgoing: This -> Others)
const outgoing = await this.referenceRepo.find({
where: { sourceId: id },
relations: ['target', 'target.type'], // Join เพื่อเอาข้อมูลเอกสารปลายทาง
});
// ดึงรายการที่มาอ้างถึงเอกสารนี้ (Incoming: Others -> This)
const incoming = await this.referenceRepo.find({
where: { targetId: id },
relations: ['source', 'source.type'], // Join เพื่อเอาข้อมูลเอกสารต้นทาง
});
return { outgoing, incoming };
}
}

View File

@@ -0,0 +1,7 @@
import { IsInt, IsNotEmpty } from 'class-validator';
export class AddReferenceDto {
@IsInt()
@IsNotEmpty()
targetId!: number;
}

View File

@@ -20,6 +20,10 @@ export class CreateCorrespondenceDto {
@IsNotEmpty()
title!: string;
@IsString()
@IsOptional()
description?: string;
@IsObject()
@IsOptional()
details?: Record<string, any>; // ข้อมูล JSON (เช่น RFI question)
@@ -28,6 +32,11 @@ export class CreateCorrespondenceDto {
@IsOptional()
isInternal?: boolean;
// ✅ เพิ่ม Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง)
@IsInt()
@IsOptional()
originatorId?: number;
// (Optional) ถ้าจะมีการแนบไฟล์มาด้วยเลย
// @IsArray()
// @IsString({ each: true })

View File

@@ -0,0 +1,24 @@
import { IsOptional, IsString, IsInt } from 'class-validator';
import { Type } from 'class-transformer'; // <--- ✅ Import จาก class-transformer
export class SearchCorrespondenceDto {
@IsOptional()
@IsString()
search?: string; // ค้นหาจาก Title หรือ Number
@IsOptional()
@Type(() => Number)
@IsInt()
typeId?: number;
@IsOptional()
@Type(() => Number)
@IsInt()
projectId?: number;
// status อาจจะซับซ้อนหน่อยเพราะอยู่ที่ Revision แต่ใส่ไว้ก่อน
@IsOptional()
@Type(() => Number)
@IsInt()
statusId?: number;
}

View File

@@ -0,0 +1,19 @@
import { Entity, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
import { Correspondence } from './correspondence.entity.js';
@Entity('correspondence_references')
export class CorrespondenceReference {
@PrimaryColumn({ name: 'src_correspondence_id' })
sourceId!: number;
@PrimaryColumn({ name: 'tgt_correspondence_id' })
targetId!: number;
@ManyToOne(() => Correspondence, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'src_correspondence_id' })
source?: Correspondence;
@ManyToOne(() => Correspondence, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'tgt_correspondence_id' })
target?: Correspondence;
}

View File

@@ -0,0 +1,76 @@
import {
Controller,
Get,
Post,
Body,
Param,
Delete,
Put,
Query,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { ContractDrawingService } from './contract-drawing.service';
import { CreateContractDrawingDto } from './dto/create-contract-drawing.dto';
import { UpdateContractDrawingDto } from './dto/update-contract-drawing.dto';
import { SearchContractDrawingDto } from './dto/search-contract-drawing.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { User } from '../user/entities/user.entity';
@ApiTags('Contract Drawings')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('drawings/contract')
export class ContractDrawingController {
constructor(
private readonly contractDrawingService: ContractDrawingService,
) {}
@Post()
@ApiOperation({ summary: 'Create new Contract Drawing' })
@RequirePermission('drawing.create') // สิทธิ์ ID 39: สร้าง/แก้ไขข้อมูลแบบ
create(
@Body() createDto: CreateContractDrawingDto,
@CurrentUser() user: User,
) {
return this.contractDrawingService.create(createDto, user);
}
@Get()
@ApiOperation({ summary: 'Search Contract Drawings' })
@RequirePermission('document.view') // สิทธิ์ ID 31: ดูเอกสารทั่วไป
findAll(@Query() searchDto: SearchContractDrawingDto) {
return this.contractDrawingService.findAll(searchDto);
}
@Get(':id')
@ApiOperation({ summary: 'Get Contract Drawing details' })
@RequirePermission('document.view')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.contractDrawingService.findOne(id);
}
@Put(':id')
@ApiOperation({ summary: 'Update Contract Drawing' })
@RequirePermission('drawing.create') // สิทธิ์ ID 39 ครอบคลุมการแก้ไขด้วย
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateDto: UpdateContractDrawingDto,
@CurrentUser() user: User,
) {
return this.contractDrawingService.update(id, updateDto, user);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete Contract Drawing (Soft Delete)' })
@RequirePermission('document.delete') // สิทธิ์ ID 34: ลบเอกสาร
remove(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User) {
return this.contractDrawingService.remove(id, user);
}
}

View File

@@ -0,0 +1,248 @@
import {
Injectable,
NotFoundException,
ConflictException,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In, Brackets } from 'typeorm';
// Entities
import { ContractDrawing } from './entities/contract-drawing.entity';
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
import { User } from '../user/entities/user.entity';
// DTOs
import { CreateContractDrawingDto } from './dto/create-contract-drawing.dto';
import { SearchContractDrawingDto } from './dto/search-contract-drawing.dto';
import { UpdateContractDrawingDto } from './dto/update-contract-drawing.dto';
// Services
import { FileStorageService } from '../../common/file-storage/file-storage.service';
@Injectable()
export class ContractDrawingService {
private readonly logger = new Logger(ContractDrawingService.name);
constructor(
@InjectRepository(ContractDrawing)
private drawingRepo: Repository<ContractDrawing>,
@InjectRepository(Attachment)
private attachmentRepo: Repository<Attachment>,
private fileStorageService: FileStorageService,
private dataSource: DataSource,
) {}
/**
* สร้างแบบสัญญาใหม่ (Create Contract Drawing)
* - ตรวจสอบเลขที่ซ้ำในโปรเจกต์
* - บันทึกข้อมูล
* - ผูกไฟล์แนบและ Commit ไฟล์จาก Temp -> Permanent
*/
async create(createDto: CreateContractDrawingDto, user: User) {
// 1. ตรวจสอบเลขที่แบบซ้ำ (Unique per Project)
const exists = await this.drawingRepo.findOne({
where: {
projectId: createDto.projectId,
contractDrawingNo: createDto.contractDrawingNo,
},
});
if (exists) {
throw new ConflictException(
`Contract Drawing No. "${createDto.contractDrawingNo}" already exists in this project.`,
);
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 2. เตรียมไฟล์แนบ
let attachments: Attachment[] = [];
if (createDto.attachmentIds?.length) {
attachments = await this.attachmentRepo.findBy({
id: In(createDto.attachmentIds),
});
}
// 3. สร้าง Entity
const drawing = queryRunner.manager.create(ContractDrawing, {
projectId: createDto.projectId,
contractDrawingNo: createDto.contractDrawingNo,
title: createDto.title,
subCategoryId: createDto.subCategoryId,
volumeId: createDto.volumeId,
updatedBy: user.user_id,
attachments: attachments,
});
const savedDrawing = await queryRunner.manager.save(drawing);
// 4. Commit Files (ย้ายไฟล์จริง)
if (createDto.attachmentIds?.length) {
// ✅ FIX TS2345: แปลง number[] เป็น string[] ก่อนส่ง
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
);
}
await queryRunner.commitTransaction();
return savedDrawing;
} catch (err) {
await queryRunner.rollbackTransaction();
// ✅ FIX TS18046: Cast err เป็น Error
this.logger.error(
`Failed to create contract drawing: ${(err as Error).message}`,
);
throw err;
} finally {
await queryRunner.release();
}
}
/**
* ค้นหาแบบสัญญา (Search & Filter)
* รองรับ Pagination และการค้นหาด้วย Text
*/
async findAll(searchDto: SearchContractDrawingDto) {
const {
projectId,
volumeId,
subCategoryId,
search,
page = 1,
pageSize = 20,
} = searchDto;
const query = this.drawingRepo
.createQueryBuilder('drawing')
.leftJoinAndSelect('drawing.attachments', 'files')
// .leftJoinAndSelect('drawing.subCategory', 'subCat')
// .leftJoinAndSelect('drawing.volume', 'vol')
.where('drawing.projectId = :projectId', { projectId });
// Filter by Volume
if (volumeId) {
query.andWhere('drawing.volumeId = :volumeId', { volumeId });
}
// Filter by SubCategory
if (subCategoryId) {
query.andWhere('drawing.subCategoryId = :subCategoryId', {
subCategoryId,
});
}
// Search Text (No. or Title)
if (search) {
query.andWhere(
new Brackets((qb) => {
qb.where('drawing.contractDrawingNo LIKE :search', {
search: `%${search}%`,
}).orWhere('drawing.title LIKE :search', { search: `%${search}%` });
}),
);
}
query.orderBy('drawing.contractDrawingNo', 'ASC');
const skip = (page - 1) * pageSize;
query.skip(skip).take(pageSize);
const [items, total] = await query.getManyAndCount();
return {
data: items,
meta: {
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
},
};
}
/**
* ดึงข้อมูลแบบรายตัว (Get One)
*/
async findOne(id: number) {
const drawing = await this.drawingRepo.findOne({
where: { id },
relations: ['attachments'], // เพิ่ม relations อื่นๆ ตามต้องการ
});
if (!drawing) {
throw new NotFoundException(`Contract Drawing ID ${id} not found`);
}
return drawing;
}
/**
* แก้ไขข้อมูลแบบ (Update)
*/
async update(id: number, updateDto: UpdateContractDrawingDto, user: User) {
const drawing = await this.findOne(id);
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Update Fields
if (updateDto.contractDrawingNo)
drawing.contractDrawingNo = updateDto.contractDrawingNo;
if (updateDto.title) drawing.title = updateDto.title;
if (updateDto.volumeId !== undefined)
drawing.volumeId = updateDto.volumeId;
if (updateDto.subCategoryId !== undefined)
drawing.subCategoryId = updateDto.subCategoryId;
drawing.updatedBy = user.user_id;
// Update Attachments (Replace logic)
if (updateDto.attachmentIds) {
const newAttachments = await this.attachmentRepo.findBy({
id: In(updateDto.attachmentIds),
});
drawing.attachments = newAttachments;
// Commit new files
// ✅ FIX TS2345: แปลง number[] เป็น string[] ก่อนส่ง
await this.fileStorageService.commit(
updateDto.attachmentIds.map(String),
);
}
const updatedDrawing = await queryRunner.manager.save(drawing);
await queryRunner.commitTransaction();
return updatedDrawing;
} catch (err) {
await queryRunner.rollbackTransaction();
// ✅ FIX TS18046: Cast err เป็น Error (Optional: Added logger here too for consistency)
this.logger.error(
`Failed to update contract drawing: ${(err as Error).message}`,
);
throw err;
} finally {
await queryRunner.release();
}
}
/**
* ลบแบบสัญญา (Soft Delete)
*/
async remove(id: number, user: User) {
const drawing = await this.findOne(id);
// บันทึกว่าใครเป็นคนลบก่อน Soft Delete (Optional)
drawing.updatedBy = user.user_id;
await this.drawingRepo.save(drawing);
return this.drawingRepo.softRemove(drawing);
}
}

View File

@@ -0,0 +1,71 @@
import {
Controller,
Get,
Post,
Body,
Query,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { DrawingMasterDataService } from './drawing-master-data.service';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
@ApiTags('Drawing Master Data')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('drawings/master')
export class DrawingMasterDataController {
// ✅ ต้องมี export ตรงนี้
constructor(private readonly masterDataService: DrawingMasterDataService) {}
// --- Contract Drawing Endpoints ---
@Get('contract/volumes')
@ApiOperation({ summary: 'List Contract Drawing Volumes' })
@RequirePermission('document.view')
getVolumes(@Query('projectId', ParseIntPipe) projectId: number) {
return this.masterDataService.findAllVolumes(projectId);
}
@Post('contract/volumes')
@ApiOperation({ summary: 'Create Volume (Admin/PM)' })
@RequirePermission('master_data.drawing_category.manage') // สิทธิ์ ID 16
createVolume(@Body() body: any) {
// ควรใช้ DTO จริงในการผลิต
return this.masterDataService.createVolume(body);
}
@Get('contract/sub-categories')
@ApiOperation({ summary: 'List Contract Drawing Sub-Categories' })
@RequirePermission('document.view')
getContractSubCats(@Query('projectId', ParseIntPipe) projectId: number) {
return this.masterDataService.findAllContractSubCats(projectId);
}
@Post('contract/sub-categories')
@ApiOperation({ summary: 'Create Contract Sub-Category (Admin/PM)' })
@RequirePermission('master_data.drawing_category.manage')
createContractSubCat(@Body() body: any) {
return this.masterDataService.createContractSubCat(body);
}
// --- Shop Drawing Endpoints ---
@Get('shop/main-categories')
@ApiOperation({ summary: 'List Shop Drawing Main Categories' })
@RequirePermission('document.view')
getShopMainCats() {
return this.masterDataService.findAllShopMainCats();
}
@Get('shop/sub-categories')
@ApiOperation({ summary: 'List Shop Drawing Sub-Categories' })
@RequirePermission('document.view')
getShopSubCats(@Query('mainCategoryId') mainCategoryId?: number) {
return this.masterDataService.findAllShopSubCats(mainCategoryId);
}
}

View File

@@ -0,0 +1,72 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptionsWhere } from 'typeorm';
// Entities
import { ContractDrawingVolume } from './entities/contract-drawing-volume.entity';
import { ContractDrawingSubCategory } from './entities/contract-drawing-sub-category.entity';
import { ShopDrawingMainCategory } from './entities/shop-drawing-main-category.entity';
import { ShopDrawingSubCategory } from './entities/shop-drawing-sub-category.entity';
@Injectable()
export class DrawingMasterDataService {
constructor(
@InjectRepository(ContractDrawingVolume)
private cdVolumeRepo: Repository<ContractDrawingVolume>,
@InjectRepository(ContractDrawingSubCategory)
private cdSubCatRepo: Repository<ContractDrawingSubCategory>,
@InjectRepository(ShopDrawingMainCategory)
private sdMainCatRepo: Repository<ShopDrawingMainCategory>,
@InjectRepository(ShopDrawingSubCategory)
private sdSubCatRepo: Repository<ShopDrawingSubCategory>,
) {}
// --- Contract Drawing Volumes ---
async findAllVolumes(projectId: number) {
return this.cdVolumeRepo.find({
where: { projectId },
order: { sortOrder: 'ASC' },
});
}
async createVolume(data: Partial<ContractDrawingVolume>) {
const volume = this.cdVolumeRepo.create(data);
return this.cdVolumeRepo.save(volume);
}
// --- Contract Drawing Sub-Categories ---
async findAllContractSubCats(projectId: number) {
return this.cdSubCatRepo.find({
where: { projectId },
order: { sortOrder: 'ASC' },
});
}
async createContractSubCat(data: Partial<ContractDrawingSubCategory>) {
const subCat = this.cdSubCatRepo.create(data);
return this.cdSubCatRepo.save(subCat);
}
// --- Shop Drawing Main Categories ---
async findAllShopMainCats() {
return this.sdMainCatRepo.find({
where: { isActive: true },
order: { sortOrder: 'ASC' },
});
}
// --- Shop Drawing Sub Categories ---
async findAllShopSubCats(mainCategoryId?: number) {
// ✅ FIX: ใช้วิธี Spread Operator เพื่อสร้าง Object เงื่อนไขที่ถูกต้องตาม Type
const where: FindOptionsWhere<ShopDrawingSubCategory> = {
isActive: true,
...(mainCategoryId ? { mainCategoryId } : {}),
};
return this.sdSubCatRepo.find({
where,
order: { sortOrder: 'ASC' },
relations: ['mainCategory'], // Load Parent Info
});
}
}

View File

@@ -0,0 +1,63 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
// Entities (Main)
import { ContractDrawing } from './entities/contract-drawing.entity';
import { ShopDrawing } from './entities/shop-drawing.entity';
import { ShopDrawingRevision } from './entities/shop-drawing-revision.entity';
// Entities (Master Data - Contract Drawing)
import { ContractDrawingVolume } from './entities/contract-drawing-volume.entity';
import { ContractDrawingSubCategory } from './entities/contract-drawing-sub-category.entity';
// Entities (Master Data - Shop Drawing) - ✅ เพิ่มใหม่
import { ShopDrawingMainCategory } from './entities/shop-drawing-main-category.entity';
import { ShopDrawingSubCategory } from './entities/shop-drawing-sub-category.entity';
// Common Entities
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
// Services
import { ShopDrawingService } from './shop-drawing.service';
import { ContractDrawingService } from './contract-drawing.service';
import { DrawingMasterDataService } from './drawing-master-data.service'; // ✅ New
// Controllers
import { ShopDrawingController } from './shop-drawing.controller';
import { ContractDrawingController } from './contract-drawing.controller';
import { DrawingMasterDataController } from './drawing-master-data.controller';
// Modules
import { FileStorageModule } from '../../common/file-storage/file-storage.module';
@Module({
imports: [
TypeOrmModule.forFeature([
// Main
ContractDrawing,
ShopDrawing,
ShopDrawingRevision,
// Master Data
ContractDrawingVolume,
ContractDrawingSubCategory,
ShopDrawingMainCategory, // ✅
ShopDrawingSubCategory, // ✅
// Common
Attachment,
]),
FileStorageModule,
],
providers: [
ShopDrawingService,
ContractDrawingService,
DrawingMasterDataService,
],
controllers: [
ShopDrawingController,
ContractDrawingController,
DrawingMasterDataController,
],
exports: [ShopDrawingService, ContractDrawingService],
})
export class DrawingModule {}

View File

@@ -0,0 +1,34 @@
import {
IsString,
IsInt,
IsOptional,
IsArray,
IsNotEmpty,
} from 'class-validator';
export class CreateContractDrawingDto {
@IsInt()
@IsNotEmpty()
projectId!: number; // ✅ ใส่ !
@IsString()
@IsNotEmpty()
contractDrawingNo!: string; // ✅ ใส่ !
@IsString()
@IsNotEmpty()
title!: string; // ✅ ใส่ !
@IsInt()
@IsOptional()
subCategoryId?: number; // ✅ ใส่ ?
@IsInt()
@IsOptional()
volumeId?: number; // ✅ ใส่ ?
@IsArray()
@IsInt({ each: true })
@IsOptional()
attachmentIds?: number[]; // ✅ ใส่ ?
}

View File

@@ -0,0 +1,30 @@
import {
IsString,
IsOptional,
IsDateString,
IsArray,
IsInt,
} from 'class-validator';
export class CreateShopDrawingRevisionDto {
@IsString()
revisionLabel!: string; // จำเป็น: ใส่ !
@IsDateString()
@IsOptional()
revisionDate?: string; // Optional: ใส่ ?
@IsString()
@IsOptional()
description?: string; // Optional: ใส่ ?
@IsArray()
@IsInt({ each: true })
@IsOptional()
contractDrawingIds?: number[]; // Optional: ใส่ ?
@IsArray()
@IsInt({ each: true })
@IsOptional()
attachmentIds?: number[]; // Optional: ใส่ ?
}

View File

@@ -0,0 +1,47 @@
import {
IsString,
IsInt,
IsOptional,
IsDateString,
IsArray,
} from 'class-validator';
export class CreateShopDrawingDto {
@IsInt()
projectId!: number; // !
@IsString()
drawingNumber!: string; // !
@IsString()
title!: string; // !
@IsInt()
mainCategoryId!: number; // !
@IsInt()
subCategoryId!: number; // !
// First Revision Data (Optional ทั้งหมด เพราะถ้าไม่ส่งมาจะ Default ให้)
@IsString()
@IsOptional()
revisionLabel?: string; // ?
@IsDateString()
@IsOptional()
revisionDate?: string; // ?
@IsString()
@IsOptional()
description?: string; // ?
@IsArray()
@IsInt({ each: true })
@IsOptional()
contractDrawingIds?: number[]; // ?
@IsArray()
@IsInt({ each: true })
@IsOptional()
attachmentIds?: number[]; // ?
}

View File

@@ -0,0 +1,33 @@
import { IsInt, IsOptional, IsString, IsNotEmpty } from 'class-validator';
import { Type } from 'class-transformer';
export class SearchContractDrawingDto {
@IsInt()
@Type(() => Number)
@IsNotEmpty()
projectId!: number; // จำเป็น: ใส่ !
@IsOptional()
@IsInt()
@Type(() => Number)
volumeId?: number; // Optional: ใส่ ?
@IsOptional()
@IsInt()
@Type(() => Number)
subCategoryId?: number; // Optional: ใส่ ?
@IsOptional()
@IsString()
search?: string; // Optional: ใส่ ?
@IsOptional()
@IsInt()
@Type(() => Number)
page: number = 1; // มีค่า Default ไม่ต้องใส่ ! หรือ ?
@IsOptional()
@IsInt()
@Type(() => Number)
pageSize: number = 20; // มีค่า Default ไม่ต้องใส่ ! หรือ ?
}

View File

@@ -0,0 +1,32 @@
import { IsInt, IsOptional, IsString } from 'class-validator';
import { Type } from 'class-transformer';
export class SearchShopDrawingDto {
@IsInt()
@Type(() => Number)
projectId!: number; // จำเป็น: ใส่ !
@IsOptional()
@IsInt()
@Type(() => Number)
mainCategoryId?: number; // Optional: ใส่ ?
@IsOptional()
@IsInt()
@Type(() => Number)
subCategoryId?: number; // Optional: ใส่ ?
@IsOptional()
@IsString()
search?: string; // Optional: ใส่ ?
@IsOptional()
@IsInt()
@Type(() => Number)
page: number = 1; // มีค่า Default
@IsOptional()
@IsInt()
@Type(() => Number)
pageSize: number = 20; // มีค่า Default
}

View File

@@ -0,0 +1,6 @@
import { PartialType } from '@nestjs/swagger';
import { CreateContractDrawingDto } from './create-contract-drawing.dto';
export class UpdateContractDrawingDto extends PartialType(
CreateContractDrawingDto,
) {}

View File

@@ -0,0 +1,41 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Project } from '../../project/entities/project.entity';
@Entity('contract_drawing_sub_cats')
export class ContractDrawingSubCategory {
@PrimaryGeneratedColumn()
id!: number; // เติม !
@Column({ name: 'project_id' })
projectId!: number; // เติม !
@Column({ name: 'sub_cat_code', length: 50 })
subCatCode!: string; // เติม !
@Column({ name: 'sub_cat_name', length: 255 })
subCatName!: string; // เติม !
@Column({ type: 'text', nullable: true })
description?: string; // Nullable ใช้ ?
@Column({ name: 'sort_order', default: 0 })
sortOrder!: number; // เติม !
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date; // เติม !
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date; // เติม !
@ManyToOne(() => Project)
@JoinColumn({ name: 'project_id' })
project!: Project; // เติม !
}

View File

@@ -0,0 +1,41 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Project } from '../../project/entities/project.entity';
@Entity('contract_drawing_volumes')
export class ContractDrawingVolume {
@PrimaryGeneratedColumn()
id!: number; // เติม !
@Column({ name: 'project_id' })
projectId!: number; // เติม !
@Column({ name: 'volume_code', length: 50 })
volumeCode!: string; // เติม !
@Column({ name: 'volume_name', length: 255 })
volumeName!: string; // เติม !
@Column({ type: 'text', nullable: true })
description?: string; // Nullable ใช้ ?
@Column({ name: 'sort_order', default: 0 })
sortOrder!: number; // เติม !
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date; // เติม !
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date; // เติม !
@ManyToOne(() => Project)
@JoinColumn({ name: 'project_id' })
project!: Project; // เติม ! (ตัวที่ Error)
}

View File

@@ -0,0 +1,76 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
ManyToOne,
JoinColumn,
ManyToMany,
JoinTable,
} from 'typeorm';
import { Project } from '../../project/entities/project.entity';
import { User } from '../../user/entities/user.entity';
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
import { ContractDrawingSubCategory } from './contract-drawing-sub-category.entity';
import { ContractDrawingVolume } from './contract-drawing-volume.entity';
@Entity('contract_drawings')
export class ContractDrawing {
@PrimaryGeneratedColumn()
id!: number; // ! ห้ามว่าง
@Column({ name: 'project_id' })
projectId!: number; // ! ห้ามว่าง
@Column({ name: 'condwg_no', length: 255 })
contractDrawingNo!: string; // ! ห้ามว่าง
@Column({ length: 255 })
title!: string; // ! ห้ามว่าง
@Column({ name: 'sub_cat_id', nullable: true })
subCategoryId?: number; // ? ว่างได้ (Nullable)
@Column({ name: 'volume_id', nullable: true })
volumeId?: number; // ? ว่างได้ (Nullable)
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date; // ! ห้ามว่าง
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date; // ! ห้ามว่าง
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt?: Date; // ? ว่างได้ (Nullable)
@Column({ name: 'updated_by', nullable: true })
updatedBy?: number; // ? ว่างได้ (Nullable)
// --- Relations ---
@ManyToOne(() => Project)
@JoinColumn({ name: 'project_id' })
project!: Project; // ! ห้ามว่าง
@ManyToOne(() => User)
@JoinColumn({ name: 'updated_by' })
updater?: User; // ? ว่างได้
@ManyToOne(() => ContractDrawingSubCategory)
@JoinColumn({ name: 'sub_cat_id' })
subCategory?: ContractDrawingSubCategory; // ? ว่างได้ (สัมพันธ์กับ subCategoryId)
@ManyToOne(() => ContractDrawingVolume)
@JoinColumn({ name: 'volume_id' })
volume?: ContractDrawingVolume; // ? แก้ไขตรงนี้: ใส่ ? เพราะ volumeId เป็น Nullable
@ManyToMany(() => Attachment)
@JoinTable({
name: 'contract_drawing_attachments',
joinColumn: { name: 'contract_drawing_id', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'attachment_id', referencedColumnName: 'id' },
})
attachments!: Attachment[]; // ! ห้ามว่าง (TypeORM จะ return [] ถ้าไม่มี)
}

View File

@@ -0,0 +1,34 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('shop_drawing_main_categories')
export class ShopDrawingMainCategory {
@PrimaryGeneratedColumn()
id!: number; // เติม !
@Column({ name: 'main_category_code', length: 50, unique: true })
mainCategoryCode!: string; // เติม !
@Column({ name: 'main_category_name', length: 255 })
mainCategoryName!: string; // เติม !
@Column({ type: 'text', nullable: true })
description?: string; // nullable
@Column({ name: 'sort_order', default: 0 })
sortOrder!: number; // เติม !
@Column({ name: 'is_active', default: true })
isActive!: boolean; // เติม !
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date; // เติม !
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date; // เติม !
}

View File

@@ -0,0 +1,71 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
ManyToMany,
JoinTable,
} from 'typeorm';
import { ShopDrawing } from './shop-drawing.entity';
import { ContractDrawing } from './contract-drawing.entity';
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
@Entity('shop_drawing_revisions')
export class ShopDrawingRevision {
@PrimaryGeneratedColumn()
id!: number; // เติม !
@Column({ name: 'shop_drawing_id' })
shopDrawingId!: number; // เติม !
@Column({ name: 'revision_number' })
revisionNumber!: number; // เติม !
@Column({ name: 'revision_label', length: 10, nullable: true })
revisionLabel?: string; // nullable ใช้ ?
@Column({ name: 'revision_date', type: 'date', nullable: true })
revisionDate?: Date; // nullable ใช้ ?
@Column({ type: 'text', nullable: true })
description?: string; // nullable ใช้ ?
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date; // เติม !
// Relations
@ManyToOne(() => ShopDrawing, (shopDrawing) => shopDrawing.revisions, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'shop_drawing_id' })
shopDrawing!: ShopDrawing; // เติม !
// References to Contract Drawings (M:N)
@ManyToMany(() => ContractDrawing)
@JoinTable({
name: 'shop_drawing_revision_contract_refs',
joinColumn: {
name: 'shop_drawing_revision_id',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'contract_drawing_id',
referencedColumnName: 'id',
},
})
contractDrawings!: ContractDrawing[]; // เติม !
// Attachments (M:N)
@ManyToMany(() => Attachment)
@JoinTable({
name: 'shop_drawing_revision_attachments',
joinColumn: {
name: 'shop_drawing_revision_id',
referencedColumnName: 'id',
},
inverseJoinColumn: { name: 'attachment_id', referencedColumnName: 'id' },
})
attachments!: Attachment[]; // เติม ! (ตัวที่ error)
}

View File

@@ -0,0 +1,45 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
@Entity('shop_drawing_sub_categories')
export class ShopDrawingSubCategory {
@PrimaryGeneratedColumn()
id!: number; // เติม ! (ตัวที่ error)
@Column({ name: 'sub_category_code', length: 50, unique: true })
subCategoryCode!: string; // เติม !
@Column({ name: 'sub_category_name', length: 255 })
subCategoryName!: string; // เติม !
@Column({ name: 'main_category_id' })
mainCategoryId!: number; // เติม !
@Column({ type: 'text', nullable: true })
description?: string; // nullable ใช้ ?
@Column({ name: 'sort_order', default: 0 })
sortOrder!: number; // เติม !
@Column({ name: 'is_active', default: true })
isActive!: boolean; // เติม !
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date; // เติม !
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date; // เติม !
// Relation to Main Category
@ManyToOne(() => ShopDrawingMainCategory)
@JoinColumn({ name: 'main_category_id' })
mainCategory!: ShopDrawingMainCategory; // เติม !
}

View File

@@ -0,0 +1,67 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
OneToMany,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ShopDrawingRevision } from './shop-drawing-revision.entity';
import { Project } from '../../project/entities/project.entity';
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
import { ShopDrawingSubCategory } from './shop-drawing-sub-category.entity';
@Entity('shop_drawings')
export class ShopDrawing {
@PrimaryGeneratedColumn()
id!: number; // เติม !
@Column({ name: 'project_id' })
projectId!: number; // เติม !
@Column({ name: 'drawing_number', length: 100, unique: true })
drawingNumber!: string; // เติม !
@Column({ length: 500 })
title!: string; // เติม !
@Column({ name: 'main_category_id' })
mainCategoryId!: number; // เติม !
@Column({ name: 'sub_category_id' })
subCategoryId!: number; // เติม !
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date; // เติม !
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date; // เติม !
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt?: Date; // nullable
@Column({ name: 'updated_by', nullable: true })
updatedBy?: number; // nullable
// --- Relations ---
@ManyToOne(() => Project)
@JoinColumn({ name: 'project_id' })
project!: Project; // เติม ! (ตัวที่ error)
@ManyToOne(() => ShopDrawingMainCategory)
@JoinColumn({ name: 'main_category_id' })
mainCategory!: ShopDrawingMainCategory; // เติม !
@ManyToOne(() => ShopDrawingSubCategory)
@JoinColumn({ name: 'sub_category_id' })
subCategory!: ShopDrawingSubCategory; // เติม !
@OneToMany(() => ShopDrawingRevision, (revision) => revision.shopDrawing, {
cascade: true,
})
revisions!: ShopDrawingRevision[]; // เติม !
}

View File

@@ -0,0 +1,61 @@
import {
Controller,
Get,
Post,
Body,
Param,
Query,
UseGuards,
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { ShopDrawingService } from './shop-drawing.service';
import { CreateShopDrawingDto } from './dto/create-shop-drawing.dto';
import { SearchShopDrawingDto } from './dto/search-shop-drawing.dto';
import { CreateShopDrawingRevisionDto } from './dto/create-shop-drawing-revision.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RbacGuard } from '../../common/guards/rbac.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { User } from '../user/entities/user.entity';
@ApiTags('Shop Drawings')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RbacGuard)
@Controller('drawings/shop')
export class ShopDrawingController {
constructor(private readonly shopDrawingService: ShopDrawingService) {}
@Post()
@ApiOperation({ summary: 'Create new Shop Drawing with initial revision' })
@RequirePermission('drawing.create') // อ้างอิง Permission จาก Seed
create(@Body() createDto: CreateShopDrawingDto, @CurrentUser() user: User) {
return this.shopDrawingService.create(createDto, user);
}
@Get()
@ApiOperation({ summary: 'Search Shop Drawings' })
@RequirePermission('drawing.view')
findAll(@Query() searchDto: SearchShopDrawingDto) {
return this.shopDrawingService.findAll(searchDto);
}
@Get(':id')
@ApiOperation({ summary: 'Get Shop Drawing details with revisions' })
@RequirePermission('drawing.view')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.shopDrawingService.findOne(id);
}
@Post(':id/revisions')
@ApiOperation({ summary: 'Add new revision to existing Shop Drawing' })
@RequirePermission('drawing.create') // หรือ drawing.edit ตาม Logic องค์กร
createRevision(
@Param('id', ParseIntPipe) id: number,
@Body() createRevisionDto: CreateShopDrawingRevisionDto,
) {
return this.shopDrawingService.createRevision(id, createRevisionDto);
}
}

View File

@@ -0,0 +1,321 @@
import {
Injectable,
NotFoundException,
BadRequestException,
InternalServerErrorException,
ConflictException,
Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In, Brackets } from 'typeorm';
// Entities
import { ShopDrawing } from './entities/shop-drawing.entity';
import { ShopDrawingRevision } from './entities/shop-drawing-revision.entity';
import { ContractDrawing } from './entities/contract-drawing.entity';
import { Attachment } from '../../common/file-storage/entities/attachment.entity';
import { User } from '../user/entities/user.entity';
// DTOs
import { CreateShopDrawingDto } from './dto/create-shop-drawing.dto';
import { CreateShopDrawingRevisionDto } from './dto/create-shop-drawing-revision.dto';
import { SearchShopDrawingDto } from './dto/search-shop-drawing.dto';
// Services
import { FileStorageService } from '../../common/file-storage/file-storage.service';
@Injectable()
export class ShopDrawingService {
private readonly logger = new Logger(ShopDrawingService.name);
constructor(
@InjectRepository(ShopDrawing)
private shopDrawingRepo: Repository<ShopDrawing>,
@InjectRepository(ShopDrawingRevision)
private revisionRepo: Repository<ShopDrawingRevision>,
@InjectRepository(ContractDrawing)
private contractDrawingRepo: Repository<ContractDrawing>,
@InjectRepository(Attachment)
private attachmentRepo: Repository<Attachment>,
private fileStorageService: FileStorageService,
private dataSource: DataSource,
) {}
/**
* สร้าง Shop Drawing ใหม่ พร้อม Revision แรก (Rev 0)
* ทำงานภายใต้ Database Transaction เดียวกัน
*/
async create(createDto: CreateShopDrawingDto, user: User) {
// 1. ตรวจสอบเลขที่แบบซ้ำ (Unique Check)
const exists = await this.shopDrawingRepo.findOne({
where: { drawingNumber: createDto.drawingNumber },
});
if (exists) {
throw new ConflictException(
`Drawing number "${createDto.drawingNumber}" already exists.`,
);
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 2. เตรียมข้อมูล Relations (Contract Drawings & Attachments)
let contractDrawings: ContractDrawing[] = [];
if (createDto.contractDrawingIds?.length) {
contractDrawings = await this.contractDrawingRepo.findBy({
id: In(createDto.contractDrawingIds),
});
}
let attachments: Attachment[] = [];
if (createDto.attachmentIds?.length) {
attachments = await this.attachmentRepo.findBy({
id: In(createDto.attachmentIds),
});
}
// 3. สร้าง Master Shop Drawing
const shopDrawing = queryRunner.manager.create(ShopDrawing, {
projectId: createDto.projectId,
drawingNumber: createDto.drawingNumber,
title: createDto.title,
mainCategoryId: createDto.mainCategoryId,
subCategoryId: createDto.subCategoryId,
updatedBy: user.user_id,
});
const savedShopDrawing = await queryRunner.manager.save(shopDrawing);
// 4. สร้าง First Revision (Rev 0)
const revision = queryRunner.manager.create(ShopDrawingRevision, {
shopDrawingId: savedShopDrawing.id,
revisionNumber: 0, // เริ่มต้นที่ 0 เสมอ
revisionLabel: createDto.revisionLabel || '0',
revisionDate: createDto.revisionDate
? new Date(createDto.revisionDate)
: new Date(),
description: createDto.description,
contractDrawings: contractDrawings, // ผูก M:N Relation
attachments: attachments, // ผูก M:N Relation
});
await queryRunner.manager.save(revision);
// 5. Commit Files (ย้ายไฟล์จาก Temp -> Permanent)
if (createDto.attachmentIds?.length) {
// ✅ FIX: ใช้ commitFiles และแปลง number[] -> string[]
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
);
}
await queryRunner.commitTransaction();
return {
...savedShopDrawing,
currentRevision: revision,
};
} catch (err) {
await queryRunner.rollbackTransaction();
// ✅ FIX: Cast err เป็น Error
this.logger.error(
`Failed to create shop drawing: ${(err as Error).message}`,
);
throw err;
} finally {
await queryRunner.release();
}
}
/**
* เพิ่ม Revision ใหม่ให้กับ Shop Drawing เดิม (Add Revision)
* เช่น Rev 0 -> Rev A
*/
async createRevision(
shopDrawingId: number,
createDto: CreateShopDrawingRevisionDto,
) {
// 1. ตรวจสอบว่ามี Master Drawing อยู่จริง
const shopDrawing = await this.shopDrawingRepo.findOneBy({
id: shopDrawingId,
});
if (!shopDrawing) {
throw new NotFoundException('Shop Drawing not found');
}
// 2. ตรวจสอบ Label ซ้ำใน Drawing เดียวกัน
const exists = await this.revisionRepo.findOne({
where: { shopDrawingId, revisionLabel: createDto.revisionLabel },
});
if (exists) {
throw new ConflictException(
`Revision label "${createDto.revisionLabel}" already exists for this drawing.`,
);
}
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 3. เตรียม Relations
let contractDrawings: ContractDrawing[] = [];
if (createDto.contractDrawingIds?.length) {
contractDrawings = await this.contractDrawingRepo.findBy({
id: In(createDto.contractDrawingIds),
});
}
let attachments: Attachment[] = [];
if (createDto.attachmentIds?.length) {
attachments = await this.attachmentRepo.findBy({
id: In(createDto.attachmentIds),
});
}
// 4. หา Revision Number ล่าสุดเพื่อ +1 (Running Number ภายใน)
const latestRev = await this.revisionRepo.findOne({
where: { shopDrawingId },
order: { revisionNumber: 'DESC' },
});
const nextRevNum = (latestRev?.revisionNumber ?? -1) + 1;
// 5. บันทึก Revision ใหม่
const revision = queryRunner.manager.create(ShopDrawingRevision, {
shopDrawingId,
revisionNumber: nextRevNum,
revisionLabel: createDto.revisionLabel,
revisionDate: createDto.revisionDate
? new Date(createDto.revisionDate)
: new Date(),
description: createDto.description,
contractDrawings: contractDrawings,
attachments: attachments,
});
await queryRunner.manager.save(revision);
// 6. Commit Files
if (createDto.attachmentIds?.length) {
// ✅ FIX: ใช้ commitFiles และแปลง number[] -> string[]
await this.fileStorageService.commit(
createDto.attachmentIds.map(String),
);
}
await queryRunner.commitTransaction();
return revision;
} catch (err) {
await queryRunner.rollbackTransaction();
// ✅ FIX: Cast err เป็น Error
this.logger.error(`Failed to create revision: ${(err as Error).message}`);
throw err;
} finally {
await queryRunner.release();
}
}
/**
* ค้นหา Shop Drawing (Search & Filter)
* รองรับการค้นหาด้วย Text และกรองตาม Category
*/
async findAll(searchDto: SearchShopDrawingDto) {
const {
projectId,
mainCategoryId,
subCategoryId,
search,
page = 1,
pageSize = 20,
} = searchDto;
const query = this.shopDrawingRepo
.createQueryBuilder('sd')
.leftJoinAndSelect('sd.mainCategory', 'mainCat')
.leftJoinAndSelect('sd.subCategory', 'subCat')
.leftJoinAndSelect('sd.revisions', 'rev')
.where('sd.projectId = :projectId', { projectId });
if (mainCategoryId) {
query.andWhere('sd.mainCategoryId = :mainCategoryId', { mainCategoryId });
}
if (subCategoryId) {
query.andWhere('sd.subCategoryId = :subCategoryId', { subCategoryId });
}
if (search) {
query.andWhere(
new Brackets((qb) => {
qb.where('sd.drawingNumber LIKE :search', {
search: `%${search}%`,
}).orWhere('sd.title LIKE :search', { search: `%${search}%` });
}),
);
}
query.orderBy('sd.updatedAt', 'DESC');
const skip = (page - 1) * pageSize;
query.skip(skip).take(pageSize);
const [items, total] = await query.getManyAndCount();
// Transform Data: เลือก Revision ล่าสุดมาแสดงเป็น currentRevision
const transformedItems = items.map((item) => {
item.revisions.sort((a, b) => b.revisionNumber - a.revisionNumber);
const currentRevision = item.revisions[0];
return {
...item,
currentRevision,
revisions: undefined,
};
});
return {
data: transformedItems,
meta: {
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
},
};
}
/**
* ดูรายละเอียด Shop Drawing (Get One)
*/
async findOne(id: number) {
const shopDrawing = await this.shopDrawingRepo.findOne({
where: { id },
relations: [
'mainCategory',
'subCategory',
'revisions',
'revisions.attachments',
'revisions.contractDrawings',
],
order: {
revisions: { revisionNumber: 'DESC' },
},
});
if (!shopDrawing) {
throw new NotFoundException(`Shop Drawing ID ${id} not found`);
}
return shopDrawing;
}
/**
* ลบ Shop Drawing (Soft Delete)
*/
async remove(id: number, user: User) {
const shopDrawing = await this.findOne(id);
shopDrawing.updatedBy = user.user_id;
await this.shopDrawingRepo.save(shopDrawing);
return this.shopDrawingRepo.softRemove(shopDrawing);
}
}

View File

@@ -1,7 +1,7 @@
import { Controller, Post, Body, Param, UseGuards } from '@nestjs/common';
import { JsonSchemaService } from './json-schema.service.js';
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
import { RbacGuard } from '../../common/auth/rbac.guard.js';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RbacGuard } from '../../common/guards/rbac.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
@Controller('json-schemas')

View File

@@ -1,6 +1,6 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ProjectService } from './project.service.js';
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
@Controller('projects')
@UseGuards(JwtAuthGuard)

View File

@@ -16,8 +16,8 @@ import { UpdateUserDto } from './dto/update-user.dto.js';
import { AssignRoleDto } from './dto/assign-role.dto.js'; // <--- Import DTO
import { UserAssignmentService } from './user-assignment.service.js'; // <--- Import Service ใหม่
import { JwtAuthGuard } from '../../common/auth/jwt-auth.guard.js';
import { RbacGuard } from '../../common/auth/rbac.guard.js';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
import { RbacGuard } from '../../common/guards/rbac.guard.js';
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
@Controller('users')
@@ -70,4 +70,9 @@ export class UserController {
assignRole(@Body() dto: AssignRoleDto, @Request() req: any) {
return this.assignmentService.assignRole(dto, req.user);
}
@Get('me/permissions')
@UseGuards(JwtAuthGuard) // No RbacGuard here to avoid circular dependency check issues
getMyPermissions(@Request() req: any) {
return this.userService.getUserPermissions(req.user.user_id);
}
}

View File

@@ -5,5 +5,8 @@
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^(\\.{1,2}/.*)\\.js$": "$1"
}
}

View File

@@ -0,0 +1,113 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from '../src/app.module';
import { JwtService } from '@nestjs/jwt';
import { DataSource } from 'typeorm';
import { RoutingTemplate } from '../src/modules/correspondence/entities/routing-template.entity';
import { RoutingTemplateStep } from '../src/modules/correspondence/entities/routing-template-step.entity';
describe('Phase 3 Workflow (E2E)', () => {
let app: INestApplication;
let jwtService: JwtService;
let dataSource: DataSource;
let templateId: number;
let correspondenceId: number;
// Users
const editorUser = { user_id: 3, username: 'editor01', organization_id: 41 }; // Editor01 (Org 41)
const adminUser = { user_id: 2, username: 'admin', organization_id: 1 }; // Admin (Org 1)
let editorToken: string;
let adminToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
jwtService = moduleFixture.get<JwtService>(JwtService);
dataSource = moduleFixture.get<DataSource>(DataSource);
// Generate Tokens
editorToken = jwtService.sign({
username: editorUser.username,
sub: editorUser.user_id,
});
adminToken = jwtService.sign({
username: adminUser.username,
sub: adminUser.user_id,
});
// Seed Template
const templateRepo = dataSource.getRepository(RoutingTemplate);
const stepRepo = dataSource.getRepository(RoutingTemplateStep);
const template = templateRepo.create({
templateName: 'E2E Test Template',
isActive: true,
});
const savedTemplate = await templateRepo.save(template);
templateId = savedTemplate.id;
const step = stepRepo.create({
templateId: savedTemplate.id,
sequence: 1,
toOrganizationId: adminUser.organization_id, // Send to Admin's Org
stepPurpose: 'FOR_APPROVAL',
});
await stepRepo.save(step);
});
afterAll(async () => {
// Cleanup
if (dataSource) {
const templateRepo = dataSource.getRepository(RoutingTemplate);
await templateRepo.delete(templateId);
// Correspondence cleanup might be needed if not using a test DB
}
await app.close();
});
it('/correspondences (POST) - Create Document', async () => {
const response = await request(app.getHttpServer())
.post('/correspondences')
.set('Authorization', `Bearer ${editorToken}`)
.send({
projectId: 1, // LCBP3
typeId: 1, // RFA (Assuming ID 1 exists from seed)
title: 'E2E Workflow Test Document',
details: { question: 'Testing Workflow' },
})
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('correspondenceNumber');
correspondenceId = response.body.id;
console.log('Created Correspondence ID:', correspondenceId);
});
it('/correspondences/:id/submit (POST) - Submit Workflow', async () => {
await request(app.getHttpServer())
.post(`/correspondences/${correspondenceId}/submit`)
.set('Authorization', `Bearer ${editorToken}`)
.send({
templateId: templateId,
})
.expect(201);
});
it('/correspondences/:id/workflow/action (POST) - Approve Step', async () => {
await request(app.getHttpServer())
.post(`/correspondences/${correspondenceId}/workflow/action`)
.set('Authorization', `Bearer ${adminToken}`)
.send({
action: 'APPROVE',
comment: 'E2E Approved',
})
.expect(201);
});
});

View File

@@ -0,0 +1,14 @@
import request from 'supertest';
import { AppModule } from '../src/app.module';
import { RoutingTemplate } from '../src/modules/correspondence/entities/routing-template.entity';
import { Test, TestingModule } from '@nestjs/testing';
describe('Simple Test', () => {
it('should pass', async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
expect(moduleFixture).toBeDefined();
});
});