690327:1118 Fixing Refactor ADR-019 Naming convention uuid #14
This commit is contained in:
+68
-67
@@ -2,7 +2,8 @@
|
|||||||
trigger: always_on
|
trigger: always_on
|
||||||
---
|
---
|
||||||
# NAP-DMS Project Context & Rules
|
# NAP-DMS Project Context & Rules
|
||||||
- For: Gemeni CLI and Gemini.
|
|
||||||
|
- For: Gemeni CLI and Gemini.
|
||||||
- Version: 1.8.4 (Accuracy Pass) | Last synced from repo: 2026-03-24
|
- Version: 1.8.4 (Accuracy Pass) | Last synced from repo: 2026-03-24
|
||||||
- Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3)
|
- Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3)
|
||||||
|
|
||||||
@@ -67,16 +68,16 @@ Best practice — ทำตามถ้าทำได้ ไม่ block:
|
|||||||
**LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**
|
**LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**
|
||||||
ระบบบริหารจัดการเอกสารโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3
|
ระบบบริหารจัดการเอกสารโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3
|
||||||
**Version:** 1.8.3 (Enforcement Tiers Added) | **Status:** UAT In Progress, Security Hardened (2026-03-19)
|
**Version:** 1.8.3 (Enforcement Tiers Added) | **Status:** UAT In Progress, Security Hardened (2026-03-19)
|
||||||
| Area | Status | Notes |
|
| Area | Status | Notes |
|
||||||
| ------------- | ---------------------- | ------------------------------------------------------ |
|
| ------------- | ---------------------- | ------------------------------------------------------ |
|
||||||
| Backend | ✅ Production Ready | NestJS 11, Express v5, 0 Vulnerabilities |
|
| Backend | ✅ Production Ready | NestJS 11, Express v5, 0 Vulnerabilities |
|
||||||
| Frontend | ✅ Quality Hardened | Next.js 16.2.0, React 19.2.4, 0 `any`, 0 `console.log` |
|
| Frontend | ✅ Quality Hardened | Next.js 16.2.0, React 19.2.4, 0 `any`, 0 `console.log` |
|
||||||
| Database | ✅ Schema v1.8.0 Stable | MariaDB 11.8, No-migration (ADR-009) |
|
| Database | ✅ Schema v1.8.0 Stable | MariaDB 11.8, No-migration (ADR-009) |
|
||||||
| Documentation | ✅ 10/10 Gaps Closed | Product Vision → Release Policy |
|
| Documentation | ✅ 10/10 Gaps Closed | Product Vision → Release Policy |
|
||||||
| AI Migration | 🔄 Pre-migration Setup | n8n + Ollama (ADR-017/018) |
|
| AI Migration | 🔄 Pre-migration Setup | n8n + Ollama (ADR-017/018) |
|
||||||
| Testing | 🔄 UAT In Progress | Per `01-05-acceptance-criteria.md` |
|
| Testing | 🔄 UAT In Progress | Per `01-05-acceptance-criteria.md` |
|
||||||
| Deployment | 📋 Pending Go-Live Gate | Blue-Green, QNAP Container Station |
|
| Deployment | 📋 Pending Go-Live Gate | Blue-Green, QNAP Container Station |
|
||||||
| ADR-019 UUID | ✅ All Phases Complete | Phase 5.4 done — all UUID FK issues resolved |
|
| ADR-019 UUID | ✅ All Phases Complete | Phase 5.4 done — all UUID FK issues resolved |
|
||||||
**Domain:** `np-dms.work`
|
**Domain:** `np-dms.work`
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -133,23 +134,23 @@ Best practice — ทำตามถ้าทำได้ ไม่ block:
|
|||||||
## 🗂️ Key Spec Files (Always Check Before Writing Code)
|
## 🗂️ Key Spec Files (Always Check Before Writing Code)
|
||||||
|
|
||||||
Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > others
|
Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > others
|
||||||
| เอกสาร | Path (relative to `specs/`) | ใช้เมื่อ |
|
| เอกสาร | Path (relative to `specs/`) | ใช้เมื่อ |
|
||||||
| ------------------------- | -------------------------------------------------------------------- | ----------------------------------- |
|
| ------------------------- | -------------------------------------------------------------------- | ----------------------------------- |
|
||||||
| **Glossary** | `00-Overview/00-02-glossary.md` | ตรวจคำศัพท์ Domain ก่อนเขียนเสมอ |
|
| **Glossary** | `00-Overview/00-02-glossary.md` | ตรวจคำศัพท์ Domain ก่อนเขียนเสมอ |
|
||||||
| **Schema Tables** | `03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` | ก่อนเขียน Query ทุกครั้ง |
|
| **Schema Tables** | `03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` | ก่อนเขียน Query ทุกครั้ง |
|
||||||
| **Data Dictionary** | `03-Data-and-Storage/03-01-data-dictionary.md` | ตรวจ Field Meaning + Business Rules |
|
| **Data Dictionary** | `03-Data-and-Storage/03-01-data-dictionary.md` | ตรวจ Field Meaning + Business Rules |
|
||||||
| **Seed Permissions** | `03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql` | ตรวจ CASL Permission Matrix |
|
| **Seed Permissions** | `03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql` | ตรวจ CASL Permission Matrix |
|
||||||
| **Edge Cases (37 rules)** | `01-Requirements/01-06-edge-cases-and-rules.md` | ป้องกัน Bug ทุก Flow |
|
| **Edge Cases (37 rules)** | `01-Requirements/01-06-edge-cases-and-rules.md` | ป้องกัน Bug ทุก Flow |
|
||||||
| **Migration Scope** | `03-Data-and-Storage/03-06-migration-business-scope.md` | งาน Migration Bot (20K docs) |
|
| **Migration Scope** | `03-Data-and-Storage/03-06-migration-business-scope.md` | งาน Migration Bot (20K docs) |
|
||||||
| **Release Policy** | `04-Infrastructure-OPS/04-08-release-management-policy.md` | ก่อน Deploy / Hotfix |
|
| **Release Policy** | `04-Infrastructure-OPS/04-08-release-management-policy.md` | ก่อน Deploy / Hotfix |
|
||||||
| **UAT Criteria** | `01-Requirements/01-05-acceptance-criteria.md` | ตรวจความสมบูรณ์ Feature |
|
| **UAT Criteria** | `01-Requirements/01-05-acceptance-criteria.md` | ตรวจความสมบูรณ์ Feature |
|
||||||
| **UUID Implementation** | `05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` | ADR-019 UUID Migration (Phase 1–6) |
|
| **UUID Implementation** | `05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` | ADR-019 UUID Migration (Phase 1–6) |
|
||||||
| **Backend Guidelines** | `05-Engineering-Guidelines/05-02-backend-guidelines.md` | NestJS patterns & best practices |
|
| **Backend Guidelines** | `05-Engineering-Guidelines/05-02-backend-guidelines.md` | NestJS patterns & best practices |
|
||||||
| **Frontend Guidelines** | `05-Engineering-Guidelines/05-03-frontend-guidelines.md` | Next.js patterns & best practices |
|
| **Frontend Guidelines** | `05-Engineering-Guidelines/05-03-frontend-guidelines.md` | Next.js patterns & best practices |
|
||||||
| **Testing Strategy** | `05-Engineering-Guidelines/05-04-testing-strategy.md` | Coverage goals & test patterns |
|
| **Testing Strategy** | `05-Engineering-Guidelines/05-04-testing-strategy.md` | Coverage goals & test patterns |
|
||||||
| **ADR-009 DB Strategy** | `06-Decision-Records/ADR-009-db-strategy.md` | Schema Change Process |
|
| **ADR-009 DB Strategy** | `06-Decision-Records/ADR-009-db-strategy.md` | Schema Change Process |
|
||||||
| **ADR-018 AI Boundary** | `06-Decision-Records/ADR-018-ai-boundary.md` | AI/Ollama Integration Rules |
|
| **ADR-018 AI Boundary** | `06-Decision-Records/ADR-018-ai-boundary.md` | AI/Ollama Integration Rules |
|
||||||
| **ADR-019 Hybrid ID** | `06-Decision-Records/ADR-019-hybrid-identifier-strategy.md` | Hybrid ID Strategy (INT + UUIDv7) |
|
| **ADR-019 Hybrid ID** | `06-Decision-Records/ADR-019-hybrid-identifier-strategy.md` | Hybrid ID Strategy (INT + UUIDv7) |
|
||||||
|
|
||||||
### Specs Directory Structure (Brief)
|
### Specs Directory Structure (Brief)
|
||||||
|
|
||||||
@@ -220,11 +221,11 @@ All UUID FK issues resolved — no more `parseInt()` on UUID values:
|
|||||||
### UUID Serialization Behavior (TransformInterceptor)
|
### UUID Serialization Behavior (TransformInterceptor)
|
||||||
|
|
||||||
`TransformInterceptor` uses `instanceToPlain()` — `@Exclude()` and `@Expose()` decorators are active on all responses.
|
`TransformInterceptor` uses `instanceToPlain()` — `@Exclude()` and `@Expose()` decorators are active on all responses.
|
||||||
| Entity Type | Behavior |
|
| Entity Type | Behavior |
|
||||||
| ------------------ | ---------------------------------------------------------------------- |
|
| ------------------ | ---------------------------------------------------------------------- |
|
||||||
| All entities | INT `id` has `@Exclude()` → **never appears in API response** |
|
| All entities | INT `id` has `@Exclude()` → **never appears in API response** |
|
||||||
| Project / Contract | `uuid` has `@Expose({ name: 'id' })` → response has `id` = UUID string |
|
| Project / Contract | `uuid` has `@Expose({ name: 'id' })` → response has `id` = UUID string |
|
||||||
| Other entities | Separate `uuid` field → response has `uuid`, no `id` |
|
| Other entities | Separate `uuid` field → response has `uuid`, no `id` |
|
||||||
|
|
||||||
### UUID Patterns (Backend Controller)
|
### UUID Patterns (Backend Controller)
|
||||||
|
|
||||||
@@ -311,14 +312,14 @@ Best practice — ทำตามถ้าทำได้ ไม่ block:
|
|||||||
|
|
||||||
**ข้อตกลงหลัก:**
|
**ข้อตกลงหลัก:**
|
||||||
|
|
||||||
| Target | Convention | Example |
|
| Target | Convention | Example |
|
||||||
| ----------------------- | ----------- | ------------------------------ |
|
| ----------------------- | ----------- | --------------------------- |
|
||||||
| **Files/Folders** | kebab-case | `user-service.ts` |
|
| **Files/Folders** | kebab-case | `user-service.ts` |
|
||||||
| **Classes** | PascalCase | `UserService` |
|
| **Classes** | PascalCase | `UserService` |
|
||||||
| **Variables/Functions** | camelCase | `firstName`, `getUserInfo` |
|
| **Variables/Functions** | camelCase | `firstName`, `getUserInfo` |
|
||||||
| **DB Columns** | snake_case | `user_id`, `created_at` |
|
| **DB Columns** | snake_case | `user_id`, `created_at` |
|
||||||
| **Boolean vars** | verb + noun | `isActive`, `hasPermission` |
|
| **Boolean vars** | verb + noun | `isActive`, `hasPermission` |
|
||||||
| **Code** | English | All identifiers in English |
|
| **Code** | English | All identifiers in English |
|
||||||
| **Comments/Docs** | Thai | ความคิดเห็นและเอกสารใช้ภาษาไทย |
|
| **Comments/Docs** | Thai | ความคิดเห็นและเอกสารใช้ภาษาไทย |
|
||||||
|
|
||||||
**❌ Common Violations พบบ่อย:**
|
**❌ Common Violations พบบ่อย:**
|
||||||
@@ -355,17 +356,17 @@ Best practice — ทำตามถ้าทำได้ ไม่ block:
|
|||||||
## 🏷️ Domain Terminology (Glossary)
|
## 🏷️ Domain Terminology (Glossary)
|
||||||
|
|
||||||
อ้างอิง `specs/00-Overview/00-02-glossary.md` เสมอ — ใช้ term ผิดจะทำให้ spec ไม่ตรง
|
อ้างอิง `specs/00-Overview/00-02-glossary.md` เสมอ — ใช้ term ผิดจะทำให้ spec ไม่ตรง
|
||||||
| ✅ ใช้ (Correct) | ❌ ห้ามใช้ (Wrong) |
|
| ✅ ใช้ (Correct) | ❌ ห้ามใช้ (Wrong) |
|
||||||
| ------------------ | ------------------------------------------- |
|
| ------------------ | ------------------------------------------- |
|
||||||
| Correspondence | Letter, Communication, Document (generic) |
|
| Correspondence | Letter, Communication, Document (generic) |
|
||||||
| RFA | Approval Request, Submit for Approval |
|
| RFA | Approval Request, Submit for Approval |
|
||||||
| Transmittal | Delivery Note, Cover Letter |
|
| Transmittal | Delivery Note, Cover Letter |
|
||||||
| Circulation | Distribution, Routing |
|
| Circulation | Distribution, Routing |
|
||||||
| Shop Drawing | Construction Drawing (generic) |
|
| Shop Drawing | Construction Drawing (generic) |
|
||||||
| Contract Drawing | Design Drawing, Blueprint |
|
| Contract Drawing | Design Drawing, Blueprint |
|
||||||
| Workflow Engine | Approval Flow, Process Engine |
|
| Workflow Engine | Approval Flow, Process Engine |
|
||||||
| Document Numbering | Document ID, Auto Number |
|
| Document Numbering | Document ID, Auto Number |
|
||||||
| RBAC | Permission System, Access Control (generic) |
|
| RBAC | Permission System, Access Control (generic) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -493,15 +494,15 @@ pnpm --filter frontend test:e2e # E2E tests (Playwright)
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Type | ใช้เมื่อ |
|
| Type | ใช้เมื่อ |
|
||||||
| ---------- | ---------------------------------------- |
|
| ---------- | ------------------------------------- |
|
||||||
| `feat` | เพิ่มฟีเจอร์ใหม่ |
|
| `feat` | เพิ่มฟีเจอร์ใหม่ |
|
||||||
| `fix` | แก้ bug |
|
| `fix` | แก้ bug |
|
||||||
| `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior |
|
| `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior |
|
||||||
| `docs` | แก้ไขเอกสาร |
|
| `docs` | แก้ไขเอกสาร |
|
||||||
| `test` | เพิ่ม/แก้ test |
|
| `test` | เพิ่ม/แก้ test |
|
||||||
| `chore` | งาน infra, config, dependency updates |
|
| `chore` | งาน infra, config, dependency updates |
|
||||||
| `style` | Formatting, linting (ไม่เปลี่ยน logic) |
|
| `style` | Formatting, linting (ไม่เปลี่ยน logic) |
|
||||||
| `spec` | แก้ไข specs/ documents |
|
| `spec` | แก้ไข specs/ documents |
|
||||||
| `adr` | เพิ่ม/แก้ไข Architecture Decision Record |
|
| `adr` | เพิ่ม/แก้ไข Architecture Decision Record |
|
||||||
|
|
||||||
**ตัวอย่าง:**
|
**ตัวอย่าง:**
|
||||||
@@ -559,7 +560,7 @@ adr/019-uuid-serialization-behavior
|
|||||||
|
|
||||||
## 🚫 Forbidden Actions
|
## 🚫 Forbidden Actions
|
||||||
|
|
||||||
| ❌ Forbidden | ✅ Correct Approach |
|
| ❌ Forbidden | ✅ Correct Approach |
|
||||||
| ----------------------------------------------- | --------------------------------------------------------- |
|
| ----------------------------------------------- | --------------------------------------------------------- |
|
||||||
| SQL Triggers for business logic | NestJS Service methods |
|
| SQL Triggers for business logic | NestJS Service methods |
|
||||||
| `.env` files in production | `docker-compose.yml` environment section |
|
| `.env` files in production | `docker-compose.yml` environment section |
|
||||||
@@ -576,7 +577,7 @@ adr/019-uuid-serialization-behavior
|
|||||||
| Generic domain terms (Letter, Blueprint, etc.) | Correct term from Glossary (`00-02-glossary.md`) |
|
| Generic domain terms (Letter, Blueprint, etc.) | Correct term from Glossary (`00-02-glossary.md`) |
|
||||||
| Deploying without Release Gates | Complete `04-08-release-management-policy.md` gates |
|
| Deploying without Release Gates | Complete `04-08-release-management-policy.md` gates |
|
||||||
| Starting migration without Go/No-Go Gate #1 | Gate approval first (`03-06-migration-business-scope.md`) |
|
| Starting migration without Go/No-Go Gate #1 | Gate approval first (`03-06-migration-business-scope.md`) |
|
||||||
| Closing UAT without all Acceptance Criteria ✅ | Full sign-off per `01-05-acceptance-criteria.md` |
|
| Closing UAT without all Acceptance Criteria ✅ | Full sign-off per `01-05-acceptance-criteria.md` |
|
||||||
| Modifying Migration Bot token scope | IP Whitelist + 7-day expiry only |
|
| Modifying Migration Bot token scope | IP Whitelist + 7-day expiry only |
|
||||||
| OWASP Top 10 violations | Security checklist before every PR |
|
| OWASP Top 10 violations | Security checklist before every PR |
|
||||||
|
|
||||||
@@ -615,16 +616,16 @@ adr/019-uuid-serialization-behavior
|
|||||||
## 🎯 Windsurf Context-Aware Triggers
|
## 🎯 Windsurf Context-Aware Triggers
|
||||||
|
|
||||||
เมื่อผู้ใช้ถามเกี่ยวกับ... ให้ตรวจสอบไฟล์เหล่านี้ก่อนตอบ
|
เมื่อผู้ใช้ถามเกี่ยวกับ... ให้ตรวจสอบไฟล์เหล่านี้ก่อนตอบ
|
||||||
| คำถาม/คำสั่ง | ไฟล์ที่ต้องตรวจสอบก่อน | คำตอบที่คาดหวัง |
|
| คำถาม/คำสั่ง | ไฟล์ที่ต้องตรวจสอบก่อน | คำตอบที่คาดหวัง |
|
||||||
| -------------------- | ------------------------------------------------------- | ---------------------------------------------------------- |
|
| -------------------- | ------------------------------------------------------- | ---------------------------------------------------------- |
|
||||||
| "สร้าง API ใหม่" | `05-02-backend-guidelines.md`, `schema-02-tables.sql` | NestJS Controller + Service + DTO + CASL Guard |
|
| "สร้าง API ใหม่" | `05-02-backend-guidelines.md`, `schema-02-tables.sql` | NestJS Controller + Service + DTO + CASL Guard |
|
||||||
| "แก้ฟอร์ม frontend" | `05-03-frontend-guidelines.md`, `01-06-edge-cases.md` | RHF+Zod + TanStack Query + Thai comments |
|
| "แก้ฟอร์ม frontend" | `05-03-frontend-guidelines.md`, `01-06-edge-cases.md` | RHF+Zod + TanStack Query + Thai comments |
|
||||||
| "เพิ่ม field ใหม่" | `ADR-009`, `data-dictionary.md`, `schema-02-tables.sql` | แก้ SQL โดยตรง + อัพเดท Data Dictionary + Entity |
|
| "เพิ่ม field ใหม่" | `ADR-009`, `data-dictionary.md`, `schema-02-tables.sql` | แก้ SQL โดยตรง + อัพเดท Data Dictionary + Entity |
|
||||||
| "ตรวจสอบ UUID" | `ADR-019`, `05-07-hybrid-uuid-implementation-plan.md` | UUIDv7 MariaDB native UUID + TransformInterceptor behavior |
|
| "ตรวจสอบ UUID" | `ADR-019`, `05-07-hybrid-uuid-implementation-plan.md` | UUIDv7 MariaDB native UUID + TransformInterceptor behavior |
|
||||||
| "สร้าง migration" | `ADR-009`, `03-06-migration-business-scope.md` | แก้ SQL schema โดยตรง + n8n workflow |
|
| "สร้าง migration" | `ADR-009`, `03-06-migration-business-scope.md` | แก้ SQL schema โดยตรง + n8n workflow |
|
||||||
| "ตรวจสอบ permission" | `seed-permissions.sql`, `ADR-016` | CASL 4-Level RBAC matrix |
|
| "ตรวจสอบ permission" | `seed-permissions.sql`, `ADR-016` | CASL 4-Level RBAC matrix |
|
||||||
| "deploy production" | `04-08-release-management-policy.md`, `ADR-015` | Release Gates + Blue-Green strategy |
|
| "deploy production" | `04-08-release-management-policy.md`, `ADR-015` | Release Gates + Blue-Green strategy |
|
||||||
| "เพิ่ม test" | `05-04-testing-strategy.md` | Coverage goals + test patterns |
|
| "เพิ่ม test" | `05-04-testing-strategy.md` | Coverage goals + test patterns |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -870,8 +871,8 @@ async update(uuid: string, dto: UpdateDto) {
|
|||||||
|
|
||||||
| Version | Date | Changes | Updated By |
|
| Version | Date | Changes | Updated By |
|
||||||
| ------- | ---------- | --------------------------------------------------------------------------------------------------------------------- | -------------- |
|
| ------- | ---------- | --------------------------------------------------------------------------------------------------------------------- | -------------- |
|
||||||
| 1.8.4 | 2026-03-24 | Phase 5.4→✅ DONE, Tailwind 3.4.3, ADR count(16), MariaDB UUID note, TanStack v5 patterns, formatting fix | Windsurf AI |
|
| 1.8.4 | 2026-03-24 | Phase 5.4→✅ DONE, Tailwind 3.4.3, ADR count(16), MariaDB UUID note, TanStack v5 patterns, formatting fix | Windsurf AI |
|
||||||
| 1.8.3 | 2026-03-21 | + Rule Enforcement Tiers (🔴🟡🟢), + Tiered Development Flow | Human Dev + AI |
|
| 1.8.3 | 2026-03-21 | + Rule Enforcement Tiers (🔴🟡🟢), + Tiered Development Flow | Human Dev + AI |
|
||||||
| 1.8.2 | 2026-03-21 | + Context Triggers, + Code Snippets, + Error Handling, + i18n, + Performance, + Testing Checklist, + Prompt Templates | Human Dev + AI |
|
| 1.8.2 | 2026-03-21 | + Context Triggers, + Code Snippets, + Error Handling, + i18n, + Performance, + Testing Checklist, + Prompt Templates | Human Dev + AI |
|
||||||
| 1.8.1 | 2026-03-21 | + ADR-019 UUID patterns, + Phase 5.4 pending files | Claude Sonnet |
|
| 1.8.1 | 2026-03-21 | + ADR-019 UUID patterns, + Phase 5.4 pending files | Claude Sonnet |
|
||||||
| 1.8.0 | 2026-03-19 | + Security overrides, + UAT criteria reference | Human Dev |
|
| 1.8.0 | 2026-03-19 | + Security overrides, + UAT criteria reference | Human Dev |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# NAP-DMS Project Context & Rules
|
# NAP-DMS Project Context & Rules
|
||||||
|
|
||||||
- For: Windsurf Cascade (and compatible: Codex CLI, opencode, Amp, Amazon Q, AGENTS.md tools)
|
- For: Windsurf Cascade (and compatible: Codex CLI, opencode, Amp, Antigravity, AGENTS.md tools)
|
||||||
- Version: 1.8.4 (Accuracy Pass) | Last synced from repo: 2026-03-24
|
- Version: 1.8.4 (Accuracy Pass) | Last synced from repo: 2026-03-24
|
||||||
- Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3)
|
- Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3)
|
||||||
|
|
||||||
@@ -65,16 +65,16 @@ Best practice — ทำตามถ้าทำได้ ไม่ block:
|
|||||||
**LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**
|
**LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)**
|
||||||
ระบบบริหารจัดการเอกสารโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3
|
ระบบบริหารจัดการเอกสารโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3
|
||||||
**Version:** 1.8.3 (Enforcement Tiers Added) | **Status:** UAT In Progress, Security Hardened (2026-03-19)
|
**Version:** 1.8.3 (Enforcement Tiers Added) | **Status:** UAT In Progress, Security Hardened (2026-03-19)
|
||||||
| Area | Status | Notes |
|
| Area | Status | Notes |
|
||||||
| ------------- | ---------------------- | ------------------------------------------------------ |
|
| ------------- | ---------------------- | ------------------------------------------------------ |
|
||||||
| Backend | ✅ Production Ready | NestJS 11, Express v5, 0 Vulnerabilities |
|
| Backend | ✅ Production Ready | NestJS 11, Express v5, 0 Vulnerabilities |
|
||||||
| Frontend | ✅ Quality Hardened | Next.js 16.2.0, React 19.2.4, 0 `any`, 0 `console.log` |
|
| Frontend | ✅ Quality Hardened | Next.js 16.2.0, React 19.2.4, 0 `any`, 0 `console.log` |
|
||||||
| Database | ✅ Schema v1.8.0 Stable | MariaDB 11.8, No-migration (ADR-009) |
|
| Database | ✅ Schema v1.8.0 Stable | MariaDB 11.8, No-migration (ADR-009) |
|
||||||
| Documentation | ✅ 10/10 Gaps Closed | Product Vision → Release Policy |
|
| Documentation | ✅ 10/10 Gaps Closed | Product Vision → Release Policy |
|
||||||
| AI Migration | 🔄 Pre-migration Setup | n8n + Ollama (ADR-017/018) |
|
| AI Migration | 🔄 Pre-migration Setup | n8n + Ollama (ADR-017/018) |
|
||||||
| Testing | 🔄 UAT In Progress | Per `01-05-acceptance-criteria.md` |
|
| Testing | 🔄 UAT In Progress | Per `01-05-acceptance-criteria.md` |
|
||||||
| Deployment | 📋 Pending Go-Live Gate | Blue-Green, QNAP Container Station |
|
| Deployment | 📋 Pending Go-Live Gate | Blue-Green, QNAP Container Station |
|
||||||
| ADR-019 UUID | ✅ All Phases Complete | Phase 5.4 done — all UUID FK issues resolved |
|
| ADR-019 UUID | ✅ All Phases Complete | Phase 5.4 done — all UUID FK issues resolved |
|
||||||
**Domain:** `np-dms.work`
|
**Domain:** `np-dms.work`
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -131,23 +131,23 @@ Best practice — ทำตามถ้าทำได้ ไม่ block:
|
|||||||
## 🗂️ Key Spec Files (Always Check Before Writing Code)
|
## 🗂️ Key Spec Files (Always Check Before Writing Code)
|
||||||
|
|
||||||
Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > others
|
Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > others
|
||||||
| เอกสาร | Path (relative to `specs/`) | ใช้เมื่อ |
|
| เอกสาร | Path (relative to `specs/`) | ใช้เมื่อ |
|
||||||
| ------------------------- | -------------------------------------------------------------------- | ----------------------------------- |
|
| ------------------------- | -------------------------------------------------------------------- | ----------------------------------- |
|
||||||
| **Glossary** | `00-Overview/00-02-glossary.md` | ตรวจคำศัพท์ Domain ก่อนเขียนเสมอ |
|
| **Glossary** | `00-Overview/00-02-glossary.md` | ตรวจคำศัพท์ Domain ก่อนเขียนเสมอ |
|
||||||
| **Schema Tables** | `03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` | ก่อนเขียน Query ทุกครั้ง |
|
| **Schema Tables** | `03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` | ก่อนเขียน Query ทุกครั้ง |
|
||||||
| **Data Dictionary** | `03-Data-and-Storage/03-01-data-dictionary.md` | ตรวจ Field Meaning + Business Rules |
|
| **Data Dictionary** | `03-Data-and-Storage/03-01-data-dictionary.md` | ตรวจ Field Meaning + Business Rules |
|
||||||
| **Seed Permissions** | `03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql` | ตรวจ CASL Permission Matrix |
|
| **Seed Permissions** | `03-Data-and-Storage/lcbp3-v1.8.0-seed-permissions.sql` | ตรวจ CASL Permission Matrix |
|
||||||
| **Edge Cases (37 rules)** | `01-Requirements/01-06-edge-cases-and-rules.md` | ป้องกัน Bug ทุก Flow |
|
| **Edge Cases (37 rules)** | `01-Requirements/01-06-edge-cases-and-rules.md` | ป้องกัน Bug ทุก Flow |
|
||||||
| **Migration Scope** | `03-Data-and-Storage/03-06-migration-business-scope.md` | งาน Migration Bot (20K docs) |
|
| **Migration Scope** | `03-Data-and-Storage/03-06-migration-business-scope.md` | งาน Migration Bot (20K docs) |
|
||||||
| **Release Policy** | `04-Infrastructure-OPS/04-08-release-management-policy.md` | ก่อน Deploy / Hotfix |
|
| **Release Policy** | `04-Infrastructure-OPS/04-08-release-management-policy.md` | ก่อน Deploy / Hotfix |
|
||||||
| **UAT Criteria** | `01-Requirements/01-05-acceptance-criteria.md` | ตรวจความสมบูรณ์ Feature |
|
| **UAT Criteria** | `01-Requirements/01-05-acceptance-criteria.md` | ตรวจความสมบูรณ์ Feature |
|
||||||
| **UUID Implementation** | `05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` | ADR-019 UUID Migration (Phase 1–6) |
|
| **UUID Implementation** | `05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` | ADR-019 UUID Migration (Phase 1–6) |
|
||||||
| **Backend Guidelines** | `05-Engineering-Guidelines/05-02-backend-guidelines.md` | NestJS patterns & best practices |
|
| **Backend Guidelines** | `05-Engineering-Guidelines/05-02-backend-guidelines.md` | NestJS patterns & best practices |
|
||||||
| **Frontend Guidelines** | `05-Engineering-Guidelines/05-03-frontend-guidelines.md` | Next.js patterns & best practices |
|
| **Frontend Guidelines** | `05-Engineering-Guidelines/05-03-frontend-guidelines.md` | Next.js patterns & best practices |
|
||||||
| **Testing Strategy** | `05-Engineering-Guidelines/05-04-testing-strategy.md` | Coverage goals & test patterns |
|
| **Testing Strategy** | `05-Engineering-Guidelines/05-04-testing-strategy.md` | Coverage goals & test patterns |
|
||||||
| **ADR-009 DB Strategy** | `06-Decision-Records/ADR-009-db-strategy.md` | Schema Change Process |
|
| **ADR-009 DB Strategy** | `06-Decision-Records/ADR-009-db-strategy.md` | Schema Change Process |
|
||||||
| **ADR-018 AI Boundary** | `06-Decision-Records/ADR-018-ai-boundary.md` | AI/Ollama Integration Rules |
|
| **ADR-018 AI Boundary** | `06-Decision-Records/ADR-018-ai-boundary.md` | AI/Ollama Integration Rules |
|
||||||
| **ADR-019 Hybrid ID** | `06-Decision-Records/ADR-019-hybrid-identifier-strategy.md` | Hybrid ID Strategy (INT + UUIDv7) |
|
| **ADR-019 Hybrid ID** | `06-Decision-Records/ADR-019-hybrid-identifier-strategy.md` | Hybrid ID Strategy (INT + UUIDv7) |
|
||||||
|
|
||||||
### Specs Directory Structure (Brief)
|
### Specs Directory Structure (Brief)
|
||||||
|
|
||||||
@@ -218,11 +218,11 @@ All UUID FK issues resolved — no more `parseInt()` on UUID values:
|
|||||||
### UUID Serialization Behavior (TransformInterceptor)
|
### UUID Serialization Behavior (TransformInterceptor)
|
||||||
|
|
||||||
`TransformInterceptor` uses `instanceToPlain()` — `@Exclude()` and `@Expose()` decorators are active on all responses.
|
`TransformInterceptor` uses `instanceToPlain()` — `@Exclude()` and `@Expose()` decorators are active on all responses.
|
||||||
| Entity Type | Behavior |
|
| Entity Type | Behavior |
|
||||||
| ------------------ | ---------------------------------------------------------------------- |
|
| ------------------ | ---------------------------------------------------------------------- |
|
||||||
| All entities | INT `id` has `@Exclude()` → **never appears in API response** |
|
| All entities | INT `id` has `@Exclude()` → **never appears in API response** |
|
||||||
| Project / Contract | `uuid` has `@Expose({ name: 'id' })` → response has `id` = UUID string |
|
| Project / Contract | `uuid` has `@Expose({ name: 'id' })` → response has `id` = UUID string |
|
||||||
| Other entities | Separate `uuid` field → response has `uuid`, no `id` |
|
| Other entities | Separate `uuid` field → response has `uuid`, no `id` |
|
||||||
|
|
||||||
### UUID Patterns (Backend Controller)
|
### UUID Patterns (Backend Controller)
|
||||||
|
|
||||||
@@ -309,14 +309,14 @@ Best practice — ทำตามถ้าทำได้ ไม่ block:
|
|||||||
|
|
||||||
**ข้อตกลงหลัก:**
|
**ข้อตกลงหลัก:**
|
||||||
|
|
||||||
| Target | Convention | Example |
|
| Target | Convention | Example |
|
||||||
| ----------------------- | ----------- | ------------------------------ |
|
| ----------------------- | ----------- | --------------------------- |
|
||||||
| **Files/Folders** | kebab-case | `user-service.ts` |
|
| **Files/Folders** | kebab-case | `user-service.ts` |
|
||||||
| **Classes** | PascalCase | `UserService` |
|
| **Classes** | PascalCase | `UserService` |
|
||||||
| **Variables/Functions** | camelCase | `firstName`, `getUserInfo` |
|
| **Variables/Functions** | camelCase | `firstName`, `getUserInfo` |
|
||||||
| **DB Columns** | snake_case | `user_id`, `created_at` |
|
| **DB Columns** | snake_case | `user_id`, `created_at` |
|
||||||
| **Boolean vars** | verb + noun | `isActive`, `hasPermission` |
|
| **Boolean vars** | verb + noun | `isActive`, `hasPermission` |
|
||||||
| **Code** | English | All identifiers in English |
|
| **Code** | English | All identifiers in English |
|
||||||
| **Comments/Docs** | Thai | ความคิดเห็นและเอกสารใช้ภาษาไทย |
|
| **Comments/Docs** | Thai | ความคิดเห็นและเอกสารใช้ภาษาไทย |
|
||||||
|
|
||||||
**❌ Common Violations พบบ่อย:**
|
**❌ Common Violations พบบ่อย:**
|
||||||
@@ -353,17 +353,17 @@ Best practice — ทำตามถ้าทำได้ ไม่ block:
|
|||||||
## 🏷️ Domain Terminology (Glossary)
|
## 🏷️ Domain Terminology (Glossary)
|
||||||
|
|
||||||
อ้างอิง `specs/00-Overview/00-02-glossary.md` เสมอ — ใช้ term ผิดจะทำให้ spec ไม่ตรง
|
อ้างอิง `specs/00-Overview/00-02-glossary.md` เสมอ — ใช้ term ผิดจะทำให้ spec ไม่ตรง
|
||||||
| ✅ ใช้ (Correct) | ❌ ห้ามใช้ (Wrong) |
|
| ✅ ใช้ (Correct) | ❌ ห้ามใช้ (Wrong) |
|
||||||
| ------------------ | ------------------------------------------- |
|
| ------------------ | ------------------------------------------- |
|
||||||
| Correspondence | Letter, Communication, Document (generic) |
|
| Correspondence | Letter, Communication, Document (generic) |
|
||||||
| RFA | Approval Request, Submit for Approval |
|
| RFA | Approval Request, Submit for Approval |
|
||||||
| Transmittal | Delivery Note, Cover Letter |
|
| Transmittal | Delivery Note, Cover Letter |
|
||||||
| Circulation | Distribution, Routing |
|
| Circulation | Distribution, Routing |
|
||||||
| Shop Drawing | Construction Drawing (generic) |
|
| Shop Drawing | Construction Drawing (generic) |
|
||||||
| Contract Drawing | Design Drawing, Blueprint |
|
| Contract Drawing | Design Drawing, Blueprint |
|
||||||
| Workflow Engine | Approval Flow, Process Engine |
|
| Workflow Engine | Approval Flow, Process Engine |
|
||||||
| Document Numbering | Document ID, Auto Number |
|
| Document Numbering | Document ID, Auto Number |
|
||||||
| RBAC | Permission System, Access Control (generic) |
|
| RBAC | Permission System, Access Control (generic) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -491,15 +491,15 @@ pnpm --filter frontend test:e2e # E2E tests (Playwright)
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Type | ใช้เมื่อ |
|
| Type | ใช้เมื่อ |
|
||||||
| ---------- | ---------------------------------------- |
|
| ---------- | ------------------------------------- |
|
||||||
| `feat` | เพิ่มฟีเจอร์ใหม่ |
|
| `feat` | เพิ่มฟีเจอร์ใหม่ |
|
||||||
| `fix` | แก้ bug |
|
| `fix` | แก้ bug |
|
||||||
| `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior |
|
| `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior |
|
||||||
| `docs` | แก้ไขเอกสาร |
|
| `docs` | แก้ไขเอกสาร |
|
||||||
| `test` | เพิ่ม/แก้ test |
|
| `test` | เพิ่ม/แก้ test |
|
||||||
| `chore` | งาน infra, config, dependency updates |
|
| `chore` | งาน infra, config, dependency updates |
|
||||||
| `style` | Formatting, linting (ไม่เปลี่ยน logic) |
|
| `style` | Formatting, linting (ไม่เปลี่ยน logic) |
|
||||||
| `spec` | แก้ไข specs/ documents |
|
| `spec` | แก้ไข specs/ documents |
|
||||||
| `adr` | เพิ่ม/แก้ไข Architecture Decision Record |
|
| `adr` | เพิ่ม/แก้ไข Architecture Decision Record |
|
||||||
|
|
||||||
**ตัวอย่าง:**
|
**ตัวอย่าง:**
|
||||||
@@ -557,7 +557,7 @@ adr/019-uuid-serialization-behavior
|
|||||||
|
|
||||||
## 🚫 Forbidden Actions
|
## 🚫 Forbidden Actions
|
||||||
|
|
||||||
| ❌ Forbidden | ✅ Correct Approach |
|
| ❌ Forbidden | ✅ Correct Approach |
|
||||||
| ----------------------------------------------- | --------------------------------------------------------- |
|
| ----------------------------------------------- | --------------------------------------------------------- |
|
||||||
| SQL Triggers for business logic | NestJS Service methods |
|
| SQL Triggers for business logic | NestJS Service methods |
|
||||||
| `.env` files in production | `docker-compose.yml` environment section |
|
| `.env` files in production | `docker-compose.yml` environment section |
|
||||||
@@ -574,7 +574,7 @@ adr/019-uuid-serialization-behavior
|
|||||||
| Generic domain terms (Letter, Blueprint, etc.) | Correct term from Glossary (`00-02-glossary.md`) |
|
| Generic domain terms (Letter, Blueprint, etc.) | Correct term from Glossary (`00-02-glossary.md`) |
|
||||||
| Deploying without Release Gates | Complete `04-08-release-management-policy.md` gates |
|
| Deploying without Release Gates | Complete `04-08-release-management-policy.md` gates |
|
||||||
| Starting migration without Go/No-Go Gate #1 | Gate approval first (`03-06-migration-business-scope.md`) |
|
| Starting migration without Go/No-Go Gate #1 | Gate approval first (`03-06-migration-business-scope.md`) |
|
||||||
| Closing UAT without all Acceptance Criteria ✅ | Full sign-off per `01-05-acceptance-criteria.md` |
|
| Closing UAT without all Acceptance Criteria ✅ | Full sign-off per `01-05-acceptance-criteria.md` |
|
||||||
| Modifying Migration Bot token scope | IP Whitelist + 7-day expiry only |
|
| Modifying Migration Bot token scope | IP Whitelist + 7-day expiry only |
|
||||||
| OWASP Top 10 violations | Security checklist before every PR |
|
| OWASP Top 10 violations | Security checklist before every PR |
|
||||||
|
|
||||||
@@ -613,16 +613,16 @@ adr/019-uuid-serialization-behavior
|
|||||||
## 🎯 Windsurf Context-Aware Triggers
|
## 🎯 Windsurf Context-Aware Triggers
|
||||||
|
|
||||||
เมื่อผู้ใช้ถามเกี่ยวกับ... ให้ตรวจสอบไฟล์เหล่านี้ก่อนตอบ
|
เมื่อผู้ใช้ถามเกี่ยวกับ... ให้ตรวจสอบไฟล์เหล่านี้ก่อนตอบ
|
||||||
| คำถาม/คำสั่ง | ไฟล์ที่ต้องตรวจสอบก่อน | คำตอบที่คาดหวัง |
|
| คำถาม/คำสั่ง | ไฟล์ที่ต้องตรวจสอบก่อน | คำตอบที่คาดหวัง |
|
||||||
| -------------------- | ------------------------------------------------------- | ---------------------------------------------------------- |
|
| -------------------- | ------------------------------------------------------- | ---------------------------------------------------------- |
|
||||||
| "สร้าง API ใหม่" | `05-02-backend-guidelines.md`, `schema-02-tables.sql` | NestJS Controller + Service + DTO + CASL Guard |
|
| "สร้าง API ใหม่" | `05-02-backend-guidelines.md`, `schema-02-tables.sql` | NestJS Controller + Service + DTO + CASL Guard |
|
||||||
| "แก้ฟอร์ม frontend" | `05-03-frontend-guidelines.md`, `01-06-edge-cases.md` | RHF+Zod + TanStack Query + Thai comments |
|
| "แก้ฟอร์ม frontend" | `05-03-frontend-guidelines.md`, `01-06-edge-cases.md` | RHF+Zod + TanStack Query + Thai comments |
|
||||||
| "เพิ่ม field ใหม่" | `ADR-009`, `data-dictionary.md`, `schema-02-tables.sql` | แก้ SQL โดยตรง + อัพเดท Data Dictionary + Entity |
|
| "เพิ่ม field ใหม่" | `ADR-009`, `data-dictionary.md`, `schema-02-tables.sql` | แก้ SQL โดยตรง + อัพเดท Data Dictionary + Entity |
|
||||||
| "ตรวจสอบ UUID" | `ADR-019`, `05-07-hybrid-uuid-implementation-plan.md` | UUIDv7 MariaDB native UUID + TransformInterceptor behavior |
|
| "ตรวจสอบ UUID" | `ADR-019`, `05-07-hybrid-uuid-implementation-plan.md` | UUIDv7 MariaDB native UUID + TransformInterceptor behavior |
|
||||||
| "สร้าง migration" | `ADR-009`, `03-06-migration-business-scope.md` | แก้ SQL schema โดยตรง + n8n workflow |
|
| "สร้าง migration" | `ADR-009`, `03-06-migration-business-scope.md` | แก้ SQL schema โดยตรง + n8n workflow |
|
||||||
| "ตรวจสอบ permission" | `seed-permissions.sql`, `ADR-016` | CASL 4-Level RBAC matrix |
|
| "ตรวจสอบ permission" | `seed-permissions.sql`, `ADR-016` | CASL 4-Level RBAC matrix |
|
||||||
| "deploy production" | `04-08-release-management-policy.md`, `ADR-015` | Release Gates + Blue-Green strategy |
|
| "deploy production" | `04-08-release-management-policy.md`, `ADR-015` | Release Gates + Blue-Green strategy |
|
||||||
| "เพิ่ม test" | `05-04-testing-strategy.md` | Coverage goals + test patterns |
|
| "เพิ่ม test" | `05-04-testing-strategy.md` | Coverage goals + test patterns |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -868,8 +868,8 @@ async update(uuid: string, dto: UpdateDto) {
|
|||||||
|
|
||||||
| Version | Date | Changes | Updated By |
|
| Version | Date | Changes | Updated By |
|
||||||
| ------- | ---------- | --------------------------------------------------------------------------------------------------------------------- | -------------- |
|
| ------- | ---------- | --------------------------------------------------------------------------------------------------------------------- | -------------- |
|
||||||
| 1.8.4 | 2026-03-24 | Phase 5.4→✅ DONE, Tailwind 3.4.3, ADR count(16), MariaDB UUID note, TanStack v5 patterns, formatting fix | Windsurf AI |
|
| 1.8.4 | 2026-03-24 | Phase 5.4→✅ DONE, Tailwind 3.4.3, ADR count(16), MariaDB UUID note, TanStack v5 patterns, formatting fix | Windsurf AI |
|
||||||
| 1.8.3 | 2026-03-21 | + Rule Enforcement Tiers (🔴🟡🟢), + Tiered Development Flow | Human Dev + AI |
|
| 1.8.3 | 2026-03-21 | + Rule Enforcement Tiers (🔴🟡🟢), + Tiered Development Flow | Human Dev + AI |
|
||||||
| 1.8.2 | 2026-03-21 | + Context Triggers, + Code Snippets, + Error Handling, + i18n, + Performance, + Testing Checklist, + Prompt Templates | Human Dev + AI |
|
| 1.8.2 | 2026-03-21 | + Context Triggers, + Code Snippets, + Error Handling, + i18n, + Performance, + Testing Checklist, + Prompt Templates | Human Dev + AI |
|
||||||
| 1.8.1 | 2026-03-21 | + ADR-019 UUID patterns, + Phase 5.4 pending files | Claude Sonnet |
|
| 1.8.1 | 2026-03-21 | + ADR-019 UUID patterns, + Phase 5.4 pending files | Claude Sonnet |
|
||||||
| 1.8.0 | 2026-03-19 | + Security overrides, + UAT criteria reference | Human Dev |
|
| 1.8.0 | 2026-03-19 | + Security overrides, + UAT criteria reference | Human Dev |
|
||||||
|
|||||||
@@ -96,6 +96,12 @@ export class CirculationService {
|
|||||||
|
|
||||||
async findAll(searchDto: SearchCirculationDto, user: User) {
|
async findAll(searchDto: SearchCirculationDto, user: User) {
|
||||||
const { status, correspondencePublicId, page = 1, limit = 20 } = searchDto;
|
const { status, correspondencePublicId, page = 1, limit = 20 } = searchDto;
|
||||||
|
|
||||||
|
// Handle users without primary organization gracefully
|
||||||
|
if (!user.primaryOrganizationId && !correspondencePublicId) {
|
||||||
|
return { data: [], meta: { total: 0, page, limit } };
|
||||||
|
}
|
||||||
|
|
||||||
const query = this.circulationRepo
|
const query = this.circulationRepo
|
||||||
.createQueryBuilder('c')
|
.createQueryBuilder('c')
|
||||||
.leftJoinAndSelect('c.creator', 'creator')
|
.leftJoinAndSelect('c.creator', 'creator')
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { SearchContractDto, CreateContractDto, UpdateContractDto } from '@/types/dto/contract/contract.dto';
|
import { SearchContractDto, CreateContractDto, UpdateContractDto } from '@/types/dto/contract/contract.dto';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
import { Contract, getContractPublicId, getProjectPublicId } from '@/types/contract';
|
||||||
|
|
||||||
interface _Project {
|
interface _Project {
|
||||||
publicId: string; // ADR-019: uuid exposed as 'publicId' (string)
|
publicId: string; // ADR-019: uuid exposed as 'publicId' (string)
|
||||||
@@ -43,21 +44,6 @@ interface _Project {
|
|||||||
projectName: string;
|
projectName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Contract {
|
|
||||||
id: string; // ADR-019: uuid exposed as 'id'
|
|
||||||
contractCode: string;
|
|
||||||
contractName: string;
|
|
||||||
projectId: number;
|
|
||||||
description?: string;
|
|
||||||
startDate?: string;
|
|
||||||
endDate?: string;
|
|
||||||
project?: {
|
|
||||||
id: string; // ADR-019: project uuid exposed as 'id'
|
|
||||||
projectCode: string;
|
|
||||||
projectName: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const contractSchema = z.object({
|
const contractSchema = z.object({
|
||||||
contractCode: z.string().min(1, 'Contract Code is required'),
|
contractCode: z.string().min(1, 'Contract Code is required'),
|
||||||
contractName: z.string().min(1, 'Contract Name is required'),
|
contractName: z.string().min(1, 'Contract Name is required'),
|
||||||
@@ -125,6 +111,7 @@ export default function ContractsPage() {
|
|||||||
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [editingUuid, setEditingUuid] = useState<string | null>(null);
|
const [editingUuid, setEditingUuid] = useState<string | null>(null);
|
||||||
|
const [editingContract, setEditingContract] = useState<Contract | null>(null);
|
||||||
|
|
||||||
// Stats for Delete Dialog
|
// Stats for Delete Dialog
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
@@ -137,7 +124,14 @@ export default function ContractsPage() {
|
|||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
if (contractToDelete) {
|
if (contractToDelete) {
|
||||||
deleteContract.mutate(contractToDelete.id, {
|
const contractUuid = getContractPublicId(contractToDelete);
|
||||||
|
|
||||||
|
if (!contractUuid) {
|
||||||
|
toast.error('Invalid contract UUID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteContract.mutate(contractUuid, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
setContractToDelete(null);
|
setContractToDelete(null);
|
||||||
@@ -205,9 +199,11 @@ export default function ContractsPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleEdit = (contract: Contract) => {
|
const handleEdit = (contract: Contract) => {
|
||||||
setEditingUuid(contract.id);
|
const contractUuid = getContractPublicId(contract);
|
||||||
// ADR-019: nested project exposes UUID as 'id'
|
setEditingUuid(contractUuid || null);
|
||||||
const pId = contract.project?.id || '';
|
setEditingContract(contract); // Store contract for caption display
|
||||||
|
// ADR-019: resolve nested project UUID from canonical field
|
||||||
|
const pId = getProjectPublicId(contract.project);
|
||||||
reset({
|
reset({
|
||||||
contractCode: contract.contractCode,
|
contractCode: contract.contractCode,
|
||||||
contractName: contract.contractName,
|
contractName: contract.contractName,
|
||||||
@@ -221,6 +217,7 @@ export default function ContractsPage() {
|
|||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
setEditingUuid(null);
|
setEditingUuid(null);
|
||||||
|
setEditingContract(null); // Clear editing contract
|
||||||
reset({
|
reset({
|
||||||
contractCode: '',
|
contractCode: '',
|
||||||
contractName: '',
|
contractName: '',
|
||||||
@@ -287,7 +284,7 @@ export default function ContractsPage() {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{editingUuid ? `Edit Contract: ${watch('contractCode') || '...'}` : 'New Contract'}
|
{editingUuid ? `Edit Contract: ${editingContract?.contractCode || '...'}` : 'New Contract'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|||||||
import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
|
import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
|
||||||
import { useProjects } from '@/hooks/use-projects';
|
import { useProjects } from '@/hooks/use-projects';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { Contract, getContractPublicId } from '@/types/contract';
|
||||||
|
|
||||||
export default function EditTemplatePage() {
|
export default function EditTemplatePage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -25,9 +26,9 @@ export default function EditTemplatePage() {
|
|||||||
const { data: projects = [] } = useProjects();
|
const { data: projects = [] } = useProjects();
|
||||||
const projectId = template?.projectId || 1;
|
const projectId = template?.projectId || 1;
|
||||||
const { data: contractsData } = useContracts(projectId);
|
const { data: contractsData } = useContracts(projectId);
|
||||||
const contracts = Array.isArray(contractsData) ? contractsData : [];
|
const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
|
||||||
const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined;
|
const firstContract = contracts[0];
|
||||||
const contractId = firstContract?.publicId ?? firstContract?.id;
|
const contractId = getContractPublicId(firstContract);
|
||||||
const { data: disciplines = [] } = useDisciplines(contractId);
|
const { data: disciplines = [] } = useDisciplines(contractId);
|
||||||
|
|
||||||
const selectedProjectName =
|
const selectedProjectName =
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
|
import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data';
|
||||||
import { useProjects } from '@/hooks/use-projects';
|
import { useProjects } from '@/hooks/use-projects';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { Contract, getContractPublicId } from '@/types/contract';
|
||||||
|
|
||||||
export default function NewTemplatePage() {
|
export default function NewTemplatePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -15,9 +16,9 @@ export default function NewTemplatePage() {
|
|||||||
const { data: projects = [] } = useProjects();
|
const { data: projects = [] } = useProjects();
|
||||||
const projectId = 1; // Default or sync with selection
|
const projectId = 1; // Default or sync with selection
|
||||||
const { data: contractsData } = useContracts(projectId);
|
const { data: contractsData } = useContracts(projectId);
|
||||||
const contracts = Array.isArray(contractsData) ? contractsData : [];
|
const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
|
||||||
const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined;
|
const firstContract = contracts[0];
|
||||||
const contractId = firstContract?.publicId ?? firstContract?.id;
|
const contractId = getContractPublicId(firstContract);
|
||||||
const { data: disciplines = [] } = useDisciplines(contractId);
|
const { data: disciplines = [] } = useDisciplines(contractId);
|
||||||
|
|
||||||
const selectedProjectName =
|
const selectedProjectName =
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { AuditLogsTable } from '@/components/numbering/audit-logs-table';
|
|||||||
import { VoidReplaceForm } from '@/components/numbering/void-replace-form';
|
import { VoidReplaceForm } from '@/components/numbering/void-replace-form';
|
||||||
import { CancelNumberForm } from '@/components/numbering/cancel-number-form';
|
import { CancelNumberForm } from '@/components/numbering/cancel-number-form';
|
||||||
import { BulkImportForm } from '@/components/numbering/bulk-import-form';
|
import { BulkImportForm } from '@/components/numbering/bulk-import-form';
|
||||||
|
import { Contract, getContractPublicId } from '@/types/contract';
|
||||||
|
|
||||||
export default function NumberingPage() {
|
export default function NumberingPage() {
|
||||||
const { data: projects = [] } = useProjects();
|
const { data: projects = [] } = useProjects();
|
||||||
@@ -54,9 +55,9 @@ export default function NumberingPage() {
|
|||||||
// Master Data
|
// Master Data
|
||||||
const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
|
const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
|
||||||
const { data: contractsData } = useContracts(selectedProjectId);
|
const { data: contractsData } = useContracts(selectedProjectId);
|
||||||
const contracts = Array.isArray(contractsData) ? contractsData : [];
|
const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
|
||||||
const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined;
|
const firstContract = contracts[0];
|
||||||
const contractId = firstContract?.publicId ?? firstContract?.id;
|
const contractId = getContractPublicId(firstContract);
|
||||||
const { data: disciplines = [] } = useDisciplines(contractId);
|
const { data: disciplines = [] } = useDisciplines(contractId);
|
||||||
|
|
||||||
const { data: templateResponse, isLoading: _isLoadingTemplates } = useTemplates();
|
const { data: templateResponse, isLoading: _isLoadingTemplates } = useTemplates();
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { Discipline } from '@/types/master-data';
|
import { Discipline } from '@/types/master-data';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Contract, getContractPublicId } from '@/types/contract';
|
||||||
|
|
||||||
export default function DisciplinesPage() {
|
export default function DisciplinesPage() {
|
||||||
const [selectedContractId, setSelectedContractId] = useState<string | null>(null);
|
const [selectedContractId, setSelectedContractId] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: contractsData = [] } = useContracts();
|
const { data: contractsData = [] } = useContracts();
|
||||||
// Ensure we consistently use an array
|
// Ensure we consistently use an array
|
||||||
const contracts = Array.isArray(contractsData) ? contractsData : [];
|
const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
|
||||||
|
|
||||||
const columns: ColumnDef<Discipline>[] = [
|
const columns: ColumnDef<Discipline>[] = [
|
||||||
{
|
{
|
||||||
@@ -56,10 +57,20 @@ export default function DisciplinesPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const contractOptions = contracts.map((c: { id?: number; publicId?: string; contractCode: string; contractName: string }) => ({
|
const contractOptions = contracts
|
||||||
label: `${c.contractName} (${c.contractCode})`,
|
.map((c) => {
|
||||||
value: String(c.publicId ?? c.id ?? ''),
|
const contractUuid = getContractPublicId(c);
|
||||||
}));
|
|
||||||
|
if (!contractUuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: `${c.contractName} (${c.contractCode})`,
|
||||||
|
value: contractUuid,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((option): option is { label: string; value: string } => option !== null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
@@ -84,7 +95,7 @@ export default function DisciplinesPage() {
|
|||||||
data as unknown as Parameters<typeof masterDataService.createDiscipline>[0]
|
data as unknown as Parameters<typeof masterDataService.createDiscipline>[0]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
updateFn={(_id, _data) => Promise.reject('Not implemented yet')}
|
updateFn={(id, data) => masterDataService.updateDiscipline(id, data)}
|
||||||
deleteFn={(id) => masterDataService.deleteDiscipline(id)}
|
deleteFn={(id) => masterDataService.deleteDiscipline(id)}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
filters={
|
filters={
|
||||||
@@ -98,11 +109,19 @@ export default function DisciplinesPage() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Contracts</SelectItem>
|
<SelectItem value="all">All Contracts</SelectItem>
|
||||||
{contracts.map((c: { id?: number; publicId?: string; contractCode: string; contractName: string }) => (
|
{contracts.map((c) => {
|
||||||
<SelectItem key={String(c.publicId ?? c.id ?? '')} value={String(c.publicId ?? c.id ?? '')}>
|
const contractUuid = getContractPublicId(c);
|
||||||
{c.contractName} ({c.contractCode})
|
|
||||||
</SelectItem>
|
if (!contractUuid) {
|
||||||
))}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectItem key={contractUuid} value={contractUuid}>
|
||||||
|
{c.contractName} ({c.contractCode})
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,19 +135,19 @@ export default function DisciplinesPage() {
|
|||||||
options: contractOptions,
|
options: contractOptions,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'discipline_code',
|
name: 'disciplineCode',
|
||||||
label: 'Code',
|
label: 'Code',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'code_name_th',
|
name: 'codeNameTh',
|
||||||
label: 'Name (TH)',
|
label: 'Name (TH)',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{ name: 'code_name_en', label: 'Name (EN)', type: 'text' },
|
{ name: 'codeNameEn', label: 'Name (EN)', type: 'text' },
|
||||||
{ name: 'is_active', label: 'Active', type: 'checkbox' },
|
{ name: 'isActive', label: 'Active', type: 'checkbox' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { RfaType } from '@/types/master-data';
|
import { RfaType } from '@/types/master-data';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Contract, getContractPublicId } from '@/types/contract';
|
||||||
|
|
||||||
export default function RfaTypesPage() {
|
export default function RfaTypesPage() {
|
||||||
const [selectedContractId, setSelectedContractId] = useState<string | null>(null);
|
const [selectedContractId, setSelectedContractId] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: contractsData = [] } = useContracts();
|
const { data: contractsData = [] } = useContracts();
|
||||||
// Ensure we consistently use an array
|
// Ensure we consistently use an array
|
||||||
const contracts = Array.isArray(contractsData) ? contractsData : [];
|
const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
|
||||||
|
|
||||||
const columns: ColumnDef<RfaType>[] = [
|
const columns: ColumnDef<RfaType>[] = [
|
||||||
{
|
{
|
||||||
@@ -60,10 +61,20 @@ export default function RfaTypesPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const contractOptions = contracts.map((c: { id?: number; publicId?: string; contract_name?: string; contract_code?: string; contractName?: string; contractCode?: string }) => ({
|
const contractOptions = contracts
|
||||||
label: `${c.contractName || c.contract_name} (${c.contractCode || c.contract_code})`,
|
.map((c) => {
|
||||||
value: String(c.publicId ?? c.id ?? ''),
|
const contractUuid = getContractPublicId(c);
|
||||||
}));
|
|
||||||
|
if (!contractUuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: `${c.contractName} (${c.contractCode})`,
|
||||||
|
value: contractUuid,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((option): option is { label: string; value: string } => option !== null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
@@ -99,11 +110,19 @@ export default function RfaTypesPage() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Contracts</SelectItem>
|
<SelectItem value="all">All Contracts</SelectItem>
|
||||||
{contracts.map((c: { id?: number; publicId?: string; contract_name?: string; contract_code?: string; contractName?: string; contractCode?: string }) => (
|
{contracts.map((c) => {
|
||||||
<SelectItem key={String(c.publicId ?? c.id ?? '')} value={String(c.publicId ?? c.id ?? '')}>
|
const contractUuid = getContractPublicId(c);
|
||||||
{c.contractName || c.contract_name} ({c.contractCode || c.contract_code})
|
|
||||||
</SelectItem>
|
if (!contractUuid) {
|
||||||
))}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectItem key={contractUuid} value={contractUuid}>
|
||||||
|
{c.contractName} ({c.contractCode})
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Loader2 } from 'lucide-react';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { useOrganizations, useCorrespondenceTypes, useDisciplines, useContracts } from '@/hooks/use-master-data';
|
import { useOrganizations, useCorrespondenceTypes, useDisciplines, useContracts } from '@/hooks/use-master-data';
|
||||||
import { Organization } from '@/types/organization';
|
import { Organization } from '@/types/organization';
|
||||||
|
import { Contract, getContractPublicId } from '@/types/contract';
|
||||||
|
|
||||||
// Local interfaces for Master Data since centralized ones are missing/fragmented
|
// Local interfaces for Master Data since centralized ones are missing/fragmented
|
||||||
interface CorrespondenceType {
|
interface CorrespondenceType {
|
||||||
@@ -47,10 +48,11 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
|
|||||||
const projectId = templateWithProject?.project?.id ?? templateWithProject?.project?.uuid ?? template?.projectId ?? 1;
|
const projectId = templateWithProject?.project?.id ?? templateWithProject?.project?.uuid ?? template?.projectId ?? 1;
|
||||||
const { data: organizations } = useOrganizations({ isActive: true });
|
const { data: organizations } = useOrganizations({ isActive: true });
|
||||||
const { data: correspondenceTypes } = useCorrespondenceTypes();
|
const { data: correspondenceTypes } = useCorrespondenceTypes();
|
||||||
const { data: contracts } = useContracts(projectId);
|
const { data: contractsData } = useContracts(projectId);
|
||||||
|
const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[];
|
||||||
|
|
||||||
// Use first contract ID for disciplines, fallback to 1 or undefined
|
// Use first contract ID for disciplines, fallback to 1 or undefined
|
||||||
const contractId = contracts?.[0]?.id;
|
const contractId = getContractPublicId(contracts[0]);
|
||||||
const { data: disciplines } = useDisciplines(contractId);
|
const { data: disciplines } = useDisciplines(contractId);
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useProjects } from '@/hooks/use-projects';
|
|||||||
import { CreateRfaDto } from '@/types/dto/rfa/rfa.dto';
|
import { CreateRfaDto } from '@/types/dto/rfa/rfa.dto';
|
||||||
import { useState, useEffect, type FormEvent } from 'react';
|
import { useState, useEffect, type FormEvent } from 'react';
|
||||||
import { correspondenceService } from '@/lib/services/correspondence.service';
|
import { correspondenceService } from '@/lib/services/correspondence.service';
|
||||||
|
import { Contract, getContractPublicId } from '@/types/contract';
|
||||||
|
|
||||||
const rfaSchema = z.object({
|
const rfaSchema = z.object({
|
||||||
projectId: z.string().min(1, 'Project is required'), // ADR-019: UUID
|
projectId: z.string().min(1, 'Project is required'), // ADR-019: UUID
|
||||||
@@ -47,8 +48,9 @@ type ProjectOption = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ContractOption = {
|
type ContractOption = {
|
||||||
|
publicId?: string;
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
id?: number;
|
id?: string;
|
||||||
contractName?: string;
|
contractName?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
contractCode?: string;
|
contractCode?: string;
|
||||||
@@ -182,8 +184,8 @@ export function RFAForm() {
|
|||||||
const selectedProjectId = watch('projectId');
|
const selectedProjectId = watch('projectId');
|
||||||
const { data: contractsData, isLoading: isLoadingContracts } = useContracts(selectedProjectId);
|
const { data: contractsData, isLoading: isLoadingContracts } = useContracts(selectedProjectId);
|
||||||
const contracts = dedupeByKey(
|
const contracts = dedupeByKey(
|
||||||
extractArrayData<ContractOption>(contractsData),
|
extractArrayData<ContractOption & Contract>(contractsData),
|
||||||
(contract) => contract.uuid ?? contract.id
|
(contract) => contract.publicId ?? contract.uuid ?? contract.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedContractId = watch('contractId');
|
const selectedContractId = watch('contractId');
|
||||||
@@ -415,7 +417,7 @@ export function RFAForm() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{contracts.map((c) => {
|
{contracts.map((c) => {
|
||||||
const contractValue = getOptionValue(c.uuid ?? c.id);
|
const contractValue = getOptionValue(getContractPublicId(c) || c.uuid);
|
||||||
|
|
||||||
if (!contractValue) {
|
if (!contractValue) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ import { AxiosError } from 'axios';
|
|||||||
import { organizationService } from '@/lib/services/organization.service';
|
import { organizationService } from '@/lib/services/organization.service';
|
||||||
import { projectService } from '@/lib/services/project.service';
|
import { projectService } from '@/lib/services/project.service';
|
||||||
import { contractService } from '@/lib/services/contract.service';
|
import { contractService } from '@/lib/services/contract.service';
|
||||||
|
import { Contract } from '@/types/contract';
|
||||||
|
|
||||||
|
// Helper to extract array data from various API response formats (paginated vs direct)
|
||||||
|
const extractArrayData = <T,>(value: unknown): T[] => {
|
||||||
|
if (Array.isArray(value)) return value as T[];
|
||||||
|
if (value && typeof value === 'object' && 'data' in value) {
|
||||||
|
const data = (value as { data?: unknown }).data;
|
||||||
|
if (Array.isArray(data)) return data as T[];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
export const masterDataKeys = {
|
export const masterDataKeys = {
|
||||||
all: ['masterData'] as const,
|
all: ['masterData'] as const,
|
||||||
@@ -84,12 +95,16 @@ export function useDisciplines(contractId?: number | string) {
|
|||||||
export function useProjects(isActive: boolean = true) {
|
export function useProjects(isActive: boolean = true) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['projects', { isActive }],
|
queryKey: ['projects', { isActive }],
|
||||||
queryFn: () => projectService.getAll({ isActive }),
|
queryFn: async () => {
|
||||||
|
const response = await projectService.getAll({ isActive });
|
||||||
|
// ADR-019: Handle paginated response { data: Project[], meta: {...} }
|
||||||
|
return extractArrayData(response);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useContracts(projectId?: number | string) {
|
export function useContracts(projectId?: number | string) {
|
||||||
return useQuery({
|
return useQuery<Contract[]>({
|
||||||
queryKey: ['contracts', projectId ?? 'all'],
|
queryKey: ['contracts', projectId ?? 'all'],
|
||||||
queryFn: () => contractService.getAll(projectId ? { projectId } : undefined),
|
queryFn: () => contractService.getAll(projectId ? { projectId } : undefined),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,30 @@
|
|||||||
import apiClient from '@/lib/api/client';
|
import apiClient from '@/lib/api/client';
|
||||||
import { CreateContractDto, UpdateContractDto, SearchContractDto } from '@/types/dto/contract/contract.dto';
|
import { CreateContractDto, UpdateContractDto, SearchContractDto } from '@/types/dto/contract/contract.dto';
|
||||||
|
import { Contract } from '@/types/contract';
|
||||||
|
|
||||||
|
const normalizeContract = (record: Contract): Contract => {
|
||||||
|
const publicId = record.publicId ?? record.id;
|
||||||
|
const project = record.project
|
||||||
|
? {
|
||||||
|
...record.project,
|
||||||
|
publicId: record.project.publicId ?? record.project.id,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
publicId,
|
||||||
|
project,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractContractArray = (payload: unknown): Contract[] => {
|
||||||
|
if (!Array.isArray(payload)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload.map((item) => normalizeContract(item as Contract));
|
||||||
|
};
|
||||||
|
|
||||||
export const contractService = {
|
export const contractService = {
|
||||||
/**
|
/**
|
||||||
@@ -8,10 +33,7 @@ export const contractService = {
|
|||||||
*/
|
*/
|
||||||
getAll: async (params?: SearchContractDto) => {
|
getAll: async (params?: SearchContractDto) => {
|
||||||
const response = await apiClient.get('/contracts', { params });
|
const response = await apiClient.get('/contracts', { params });
|
||||||
if (response.data && Array.isArray(response.data.data)) {
|
return extractContractArray(response.data?.data ?? response.data);
|
||||||
return response.data.data;
|
|
||||||
}
|
|
||||||
return response.data.data || response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,7 +42,8 @@ export const contractService = {
|
|||||||
*/
|
*/
|
||||||
getByUuid: async (uuid: string) => {
|
getByUuid: async (uuid: string) => {
|
||||||
const response = await apiClient.get(`/contracts/${uuid}`);
|
const response = await apiClient.get(`/contracts/${uuid}`);
|
||||||
return response.data;
|
const payload = response.data?.data ?? response.data;
|
||||||
|
return normalizeContract(payload as Contract);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -141,6 +141,12 @@ export const masterDataService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** แก้ไขสาขางาน */
|
||||||
|
updateDiscipline: async (id: number, data: Partial<CreateDisciplineDto>) => {
|
||||||
|
const response = await apiClient.patch(`/master/disciplines/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
// --- Sub-Types Management (Admin / Req 6B) ---
|
// --- Sub-Types Management (Admin / Req 6B) ---
|
||||||
|
|
||||||
/** ดึงรายชื่อประเภทย่อย (กรองตาม Contract และ Type) */
|
/** ดึงรายชื่อประเภทย่อย (กรองตาม Contract และ Type) */
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export interface ContractProjectReference {
|
||||||
|
publicId?: string;
|
||||||
|
id?: string;
|
||||||
|
projectCode: string;
|
||||||
|
projectName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Contract {
|
||||||
|
publicId?: string;
|
||||||
|
id?: string;
|
||||||
|
contractCode: string;
|
||||||
|
contractName: string;
|
||||||
|
projectId?: number | string;
|
||||||
|
description?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
project?: ContractProjectReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getContractPublicId = (contract?: Pick<Contract, 'publicId' | 'id'>): string =>
|
||||||
|
String(contract?.publicId ?? contract?.id ?? '');
|
||||||
|
|
||||||
|
export const getProjectPublicId = (
|
||||||
|
project?: Pick<ContractProjectReference, 'publicId' | 'id'>
|
||||||
|
): string => String(project?.publicId ?? project?.id ?? '');
|
||||||
Reference in New Issue
Block a user