diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index ca3df99..46686ab 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -2,7 +2,8 @@ trigger: always_on --- # 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 - 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)** ระบบบริหารจัดการเอกสารโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3 **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 | -| 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) | -| Documentation | ✅ 10/10 Gaps Closed | Product Vision → Release Policy | -| AI Migration | 🔄 Pre-migration Setup | n8n + Ollama (ADR-017/018) | -| Testing | 🔄 UAT In Progress | Per `01-05-acceptance-criteria.md` | -| 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 | +| 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` | +| Database | ✅ Schema v1.8.0 Stable | MariaDB 11.8, No-migration (ADR-009) | +| Documentation | ✅ 10/10 Gaps Closed | Product Vision → Release Policy | +| AI Migration | 🔄 Pre-migration Setup | n8n + Ollama (ADR-017/018) | +| Testing | 🔄 UAT In Progress | Per `01-05-acceptance-criteria.md` | +| 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 | **Domain:** `np-dms.work` --- @@ -133,23 +134,23 @@ Best practice — ทำตามถ้าทำได้ ไม่ block: ## 🗂️ Key Spec Files (Always Check Before Writing Code) 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 ก่อนเขียนเสมอ | -| **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 | -| **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 | -| **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 | -| **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) | -| **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 | -| **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-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) | +| **Glossary** | `00-Overview/00-02-glossary.md` | ตรวจคำศัพท์ Domain ก่อนเขียนเสมอ | +| **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 | +| **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 | +| **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 | +| **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) | +| **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 | +| **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-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) | ### Specs Directory Structure (Brief) @@ -220,11 +221,11 @@ All UUID FK issues resolved — no more `parseInt()` on UUID values: ### UUID Serialization Behavior (TransformInterceptor) `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 | -| Other entities | Separate `uuid` field → response has `uuid`, no `id` | +| Other entities | Separate `uuid` field → response has `uuid`, no `id` | ### UUID Patterns (Backend Controller) @@ -311,14 +312,14 @@ Best practice — ทำตามถ้าทำได้ ไม่ block: **ข้อตกลงหลัก:** -| Target | Convention | Example | -| ----------------------- | ----------- | ------------------------------ | -| **Files/Folders** | kebab-case | `user-service.ts` | -| **Classes** | PascalCase | `UserService` | -| **Variables/Functions** | camelCase | `firstName`, `getUserInfo` | -| **DB Columns** | snake_case | `user_id`, `created_at` | -| **Boolean vars** | verb + noun | `isActive`, `hasPermission` | -| **Code** | English | All identifiers in English | +| Target | Convention | Example | +| ----------------------- | ----------- | --------------------------- | +| **Files/Folders** | kebab-case | `user-service.ts` | +| **Classes** | PascalCase | `UserService` | +| **Variables/Functions** | camelCase | `firstName`, `getUserInfo` | +| **DB Columns** | snake_case | `user_id`, `created_at` | +| **Boolean vars** | verb + noun | `isActive`, `hasPermission` | +| **Code** | English | All identifiers in English | | **Comments/Docs** | Thai | ความคิดเห็นและเอกสารใช้ภาษาไทย | **❌ Common Violations พบบ่อย:** @@ -355,17 +356,17 @@ Best practice — ทำตามถ้าทำได้ ไม่ block: ## 🏷️ Domain Terminology (Glossary) อ้างอิง `specs/00-Overview/00-02-glossary.md` เสมอ — ใช้ term ผิดจะทำให้ spec ไม่ตรง -| ✅ ใช้ (Correct) | ❌ ห้ามใช้ (Wrong) | +| ✅ ใช้ (Correct) | ❌ ห้ามใช้ (Wrong) | | ------------------ | ------------------------------------------- | -| Correspondence | Letter, Communication, Document (generic) | -| RFA | Approval Request, Submit for Approval | -| Transmittal | Delivery Note, Cover Letter | -| Circulation | Distribution, Routing | -| Shop Drawing | Construction Drawing (generic) | -| Contract Drawing | Design Drawing, Blueprint | -| Workflow Engine | Approval Flow, Process Engine | -| Document Numbering | Document ID, Auto Number | -| RBAC | Permission System, Access Control (generic) | +| Correspondence | Letter, Communication, Document (generic) | +| RFA | Approval Request, Submit for Approval | +| Transmittal | Delivery Note, Cover Letter | +| Circulation | Distribution, Routing | +| Shop Drawing | Construction Drawing (generic) | +| Contract Drawing | Design Drawing, Blueprint | +| Workflow Engine | Approval Flow, Process Engine | +| Document Numbering | Document ID, Auto Number | +| RBAC | Permission System, Access Control (generic) | --- @@ -493,15 +494,15 @@ pnpm --filter frontend test:e2e # E2E tests (Playwright) ``` | Type | ใช้เมื่อ | -| ---------- | ---------------------------------------- | -| `feat` | เพิ่มฟีเจอร์ใหม่ | -| `fix` | แก้ bug | -| `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior | -| `docs` | แก้ไขเอกสาร | +| ---------- | ------------------------------------- | +| `feat` | เพิ่มฟีเจอร์ใหม่ | +| `fix` | แก้ bug | +| `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior | +| `docs` | แก้ไขเอกสาร | | `test` | เพิ่ม/แก้ test | -| `chore` | งาน infra, config, dependency updates | +| `chore` | งาน infra, config, dependency updates | | `style` | Formatting, linting (ไม่เปลี่ยน logic) | -| `spec` | แก้ไข specs/ documents | +| `spec` | แก้ไข specs/ documents | | `adr` | เพิ่ม/แก้ไข Architecture Decision Record | **ตัวอย่าง:** @@ -559,7 +560,7 @@ adr/019-uuid-serialization-behavior ## 🚫 Forbidden Actions -| ❌ Forbidden | ✅ Correct Approach | +| ❌ Forbidden | ✅ Correct Approach | | ----------------------------------------------- | --------------------------------------------------------- | | SQL Triggers for business logic | NestJS Service methods | | `.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`) | | 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`) | -| 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 | | OWASP Top 10 violations | Security checklist before every PR | @@ -615,16 +616,16 @@ adr/019-uuid-serialization-behavior ## 🎯 Windsurf Context-Aware Triggers เมื่อผู้ใช้ถามเกี่ยวกับ... ให้ตรวจสอบไฟล์เหล่านี้ก่อนตอบ -| คำถาม/คำสั่ง | ไฟล์ที่ต้องตรวจสอบก่อน | คำตอบที่คาดหวัง | +| คำถาม/คำสั่ง | ไฟล์ที่ต้องตรวจสอบก่อน | คำตอบที่คาดหวัง | | -------------------- | ------------------------------------------------------- | ---------------------------------------------------------- | -| "สร้าง 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 | -| "เพิ่ม 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 | -| "สร้าง migration" | `ADR-009`, `03-06-migration-business-scope.md` | แก้ SQL schema โดยตรง + n8n workflow | -| "ตรวจสอบ 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 | -| "เพิ่ม test" | `05-04-testing-strategy.md` | Coverage goals + test patterns | +| "สร้าง 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 | +| "เพิ่ม 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 | +| "สร้าง migration" | `ADR-009`, `03-06-migration-business-scope.md` | แก้ SQL schema โดยตรง + n8n workflow | +| "ตรวจสอบ 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 | +| "เพิ่ม 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 | | ------- | ---------- | --------------------------------------------------------------------------------------------------------------------- | -------------- | -| 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.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.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.0 | 2026-03-19 | + Security overrides, + UAT criteria reference | Human Dev | diff --git a/AGENTS.md b/AGENTS.md index 3018bec..1f2ad6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # 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 - 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)** ระบบบริหารจัดการเอกสารโครงการก่อสร้างท่าเรือแหลมฉบังระยะที่ 3 **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 | -| 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) | -| Documentation | ✅ 10/10 Gaps Closed | Product Vision → Release Policy | -| AI Migration | 🔄 Pre-migration Setup | n8n + Ollama (ADR-017/018) | -| Testing | 🔄 UAT In Progress | Per `01-05-acceptance-criteria.md` | -| 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 | +| 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` | +| Database | ✅ Schema v1.8.0 Stable | MariaDB 11.8, No-migration (ADR-009) | +| Documentation | ✅ 10/10 Gaps Closed | Product Vision → Release Policy | +| AI Migration | 🔄 Pre-migration Setup | n8n + Ollama (ADR-017/018) | +| Testing | 🔄 UAT In Progress | Per `01-05-acceptance-criteria.md` | +| 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 | **Domain:** `np-dms.work` --- @@ -131,23 +131,23 @@ Best practice — ทำตามถ้าทำได้ ไม่ block: ## 🗂️ Key Spec Files (Always Check Before Writing Code) 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 ก่อนเขียนเสมอ | -| **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 | -| **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 | -| **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 | -| **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) | -| **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 | -| **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-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) | +| **Glossary** | `00-Overview/00-02-glossary.md` | ตรวจคำศัพท์ Domain ก่อนเขียนเสมอ | +| **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 | +| **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 | +| **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 | +| **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) | +| **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 | +| **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-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) | ### Specs Directory Structure (Brief) @@ -218,11 +218,11 @@ All UUID FK issues resolved — no more `parseInt()` on UUID values: ### UUID Serialization Behavior (TransformInterceptor) `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 | -| Other entities | Separate `uuid` field → response has `uuid`, no `id` | +| Other entities | Separate `uuid` field → response has `uuid`, no `id` | ### UUID Patterns (Backend Controller) @@ -309,14 +309,14 @@ Best practice — ทำตามถ้าทำได้ ไม่ block: **ข้อตกลงหลัก:** -| Target | Convention | Example | -| ----------------------- | ----------- | ------------------------------ | -| **Files/Folders** | kebab-case | `user-service.ts` | -| **Classes** | PascalCase | `UserService` | -| **Variables/Functions** | camelCase | `firstName`, `getUserInfo` | -| **DB Columns** | snake_case | `user_id`, `created_at` | -| **Boolean vars** | verb + noun | `isActive`, `hasPermission` | -| **Code** | English | All identifiers in English | +| Target | Convention | Example | +| ----------------------- | ----------- | --------------------------- | +| **Files/Folders** | kebab-case | `user-service.ts` | +| **Classes** | PascalCase | `UserService` | +| **Variables/Functions** | camelCase | `firstName`, `getUserInfo` | +| **DB Columns** | snake_case | `user_id`, `created_at` | +| **Boolean vars** | verb + noun | `isActive`, `hasPermission` | +| **Code** | English | All identifiers in English | | **Comments/Docs** | Thai | ความคิดเห็นและเอกสารใช้ภาษาไทย | **❌ Common Violations พบบ่อย:** @@ -353,17 +353,17 @@ Best practice — ทำตามถ้าทำได้ ไม่ block: ## 🏷️ Domain Terminology (Glossary) อ้างอิง `specs/00-Overview/00-02-glossary.md` เสมอ — ใช้ term ผิดจะทำให้ spec ไม่ตรง -| ✅ ใช้ (Correct) | ❌ ห้ามใช้ (Wrong) | +| ✅ ใช้ (Correct) | ❌ ห้ามใช้ (Wrong) | | ------------------ | ------------------------------------------- | -| Correspondence | Letter, Communication, Document (generic) | -| RFA | Approval Request, Submit for Approval | -| Transmittal | Delivery Note, Cover Letter | -| Circulation | Distribution, Routing | -| Shop Drawing | Construction Drawing (generic) | -| Contract Drawing | Design Drawing, Blueprint | -| Workflow Engine | Approval Flow, Process Engine | -| Document Numbering | Document ID, Auto Number | -| RBAC | Permission System, Access Control (generic) | +| Correspondence | Letter, Communication, Document (generic) | +| RFA | Approval Request, Submit for Approval | +| Transmittal | Delivery Note, Cover Letter | +| Circulation | Distribution, Routing | +| Shop Drawing | Construction Drawing (generic) | +| Contract Drawing | Design Drawing, Blueprint | +| Workflow Engine | Approval Flow, Process Engine | +| Document Numbering | Document ID, Auto Number | +| RBAC | Permission System, Access Control (generic) | --- @@ -491,15 +491,15 @@ pnpm --filter frontend test:e2e # E2E tests (Playwright) ``` | Type | ใช้เมื่อ | -| ---------- | ---------------------------------------- | -| `feat` | เพิ่มฟีเจอร์ใหม่ | -| `fix` | แก้ bug | -| `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior | -| `docs` | แก้ไขเอกสาร | +| ---------- | ------------------------------------- | +| `feat` | เพิ่มฟีเจอร์ใหม่ | +| `fix` | แก้ bug | +| `refactor` | ปรับโครงสร้างโค้ด ไม่เปลี่ยน behavior | +| `docs` | แก้ไขเอกสาร | | `test` | เพิ่ม/แก้ test | -| `chore` | งาน infra, config, dependency updates | +| `chore` | งาน infra, config, dependency updates | | `style` | Formatting, linting (ไม่เปลี่ยน logic) | -| `spec` | แก้ไข specs/ documents | +| `spec` | แก้ไข specs/ documents | | `adr` | เพิ่ม/แก้ไข Architecture Decision Record | **ตัวอย่าง:** @@ -557,7 +557,7 @@ adr/019-uuid-serialization-behavior ## 🚫 Forbidden Actions -| ❌ Forbidden | ✅ Correct Approach | +| ❌ Forbidden | ✅ Correct Approach | | ----------------------------------------------- | --------------------------------------------------------- | | SQL Triggers for business logic | NestJS Service methods | | `.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`) | | 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`) | -| 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 | | OWASP Top 10 violations | Security checklist before every PR | @@ -613,16 +613,16 @@ adr/019-uuid-serialization-behavior ## 🎯 Windsurf Context-Aware Triggers เมื่อผู้ใช้ถามเกี่ยวกับ... ให้ตรวจสอบไฟล์เหล่านี้ก่อนตอบ -| คำถาม/คำสั่ง | ไฟล์ที่ต้องตรวจสอบก่อน | คำตอบที่คาดหวัง | +| คำถาม/คำสั่ง | ไฟล์ที่ต้องตรวจสอบก่อน | คำตอบที่คาดหวัง | | -------------------- | ------------------------------------------------------- | ---------------------------------------------------------- | -| "สร้าง 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 | -| "เพิ่ม 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 | -| "สร้าง migration" | `ADR-009`, `03-06-migration-business-scope.md` | แก้ SQL schema โดยตรง + n8n workflow | -| "ตรวจสอบ 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 | -| "เพิ่ม test" | `05-04-testing-strategy.md` | Coverage goals + test patterns | +| "สร้าง 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 | +| "เพิ่ม 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 | +| "สร้าง migration" | `ADR-009`, `03-06-migration-business-scope.md` | แก้ SQL schema โดยตรง + n8n workflow | +| "ตรวจสอบ 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 | +| "เพิ่ม 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 | | ------- | ---------- | --------------------------------------------------------------------------------------------------------------------- | -------------- | -| 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.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.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.0 | 2026-03-19 | + Security overrides, + UAT criteria reference | Human Dev | diff --git a/backend/src/modules/circulation/circulation.service.ts b/backend/src/modules/circulation/circulation.service.ts index a39d72a..7c67035 100644 --- a/backend/src/modules/circulation/circulation.service.ts +++ b/backend/src/modules/circulation/circulation.service.ts @@ -96,6 +96,12 @@ export class CirculationService { async findAll(searchDto: SearchCirculationDto, user: User) { 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 .createQueryBuilder('c') .leftJoinAndSelect('c.creator', 'creator') diff --git a/frontend/app/(admin)/admin/doc-control/contracts/page.tsx b/frontend/app/(admin)/admin/doc-control/contracts/page.tsx index ae7535a..d1557e1 100644 --- a/frontend/app/(admin)/admin/doc-control/contracts/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/contracts/page.tsx @@ -36,6 +36,7 @@ import { import { Skeleton } from '@/components/ui/skeleton'; import { SearchContractDto, CreateContractDto, UpdateContractDto } from '@/types/dto/contract/contract.dto'; import { AxiosError } from 'axios'; +import { Contract, getContractPublicId, getProjectPublicId } from '@/types/contract'; interface _Project { publicId: string; // ADR-019: uuid exposed as 'publicId' (string) @@ -43,21 +44,6 @@ interface _Project { 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({ contractCode: z.string().min(1, 'Contract Code 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 [editingUuid, setEditingUuid] = useState(null); + const [editingContract, setEditingContract] = useState(null); // Stats for Delete Dialog const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -137,7 +124,14 @@ export default function ContractsPage() { const confirmDelete = () => { if (contractToDelete) { - deleteContract.mutate(contractToDelete.id, { + const contractUuid = getContractPublicId(contractToDelete); + + if (!contractUuid) { + toast.error('Invalid contract UUID'); + return; + } + + deleteContract.mutate(contractUuid, { onSuccess: () => { setDeleteDialogOpen(false); setContractToDelete(null); @@ -205,9 +199,11 @@ export default function ContractsPage() { ]; const handleEdit = (contract: Contract) => { - setEditingUuid(contract.id); - // ADR-019: nested project exposes UUID as 'id' - const pId = contract.project?.id || ''; + const contractUuid = getContractPublicId(contract); + setEditingUuid(contractUuid || null); + setEditingContract(contract); // Store contract for caption display + // ADR-019: resolve nested project UUID from canonical field + const pId = getProjectPublicId(contract.project); reset({ contractCode: contract.contractCode, contractName: contract.contractName, @@ -221,6 +217,7 @@ export default function ContractsPage() { const handleCreate = () => { setEditingUuid(null); + setEditingContract(null); // Clear editing contract reset({ contractCode: '', contractName: '', @@ -287,7 +284,7 @@ export default function ContractsPage() { - {editingUuid ? `Edit Contract: ${watch('contractCode') || '...'}` : 'New Contract'} + {editingUuid ? `Edit Contract: ${editingContract?.contractCode || '...'}` : 'New Contract'}
diff --git a/frontend/app/(admin)/admin/doc-control/numbering/[id]/edit/page.tsx b/frontend/app/(admin)/admin/doc-control/numbering/[id]/edit/page.tsx index 25581da..d1baf5a 100644 --- a/frontend/app/(admin)/admin/doc-control/numbering/[id]/edit/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/numbering/[id]/edit/page.tsx @@ -12,6 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data'; import { useProjects } from '@/hooks/use-projects'; import { toast } from 'sonner'; +import { Contract, getContractPublicId } from '@/types/contract'; export default function EditTemplatePage() { const params = useParams(); @@ -25,9 +26,9 @@ export default function EditTemplatePage() { const { data: projects = [] } = useProjects(); const projectId = template?.projectId || 1; const { data: contractsData } = useContracts(projectId); - const contracts = Array.isArray(contractsData) ? contractsData : []; - const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined; - const contractId = firstContract?.publicId ?? firstContract?.id; + const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[]; + const firstContract = contracts[0]; + const contractId = getContractPublicId(firstContract); const { data: disciplines = [] } = useDisciplines(contractId); const selectedProjectName = diff --git a/frontend/app/(admin)/admin/doc-control/numbering/new/page.tsx b/frontend/app/(admin)/admin/doc-control/numbering/new/page.tsx index e8deace..c74bbac 100644 --- a/frontend/app/(admin)/admin/doc-control/numbering/new/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/numbering/new/page.tsx @@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation'; import { useCorrespondenceTypes, useContracts, useDisciplines } from '@/hooks/use-master-data'; import { useProjects } from '@/hooks/use-projects'; import { toast } from 'sonner'; +import { Contract, getContractPublicId } from '@/types/contract'; export default function NewTemplatePage() { const router = useRouter(); @@ -15,9 +16,9 @@ export default function NewTemplatePage() { const { data: projects = [] } = useProjects(); const projectId = 1; // Default or sync with selection const { data: contractsData } = useContracts(projectId); - const contracts = Array.isArray(contractsData) ? contractsData : []; - const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined; - const contractId = firstContract?.publicId ?? firstContract?.id; + const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[]; + const firstContract = contracts[0]; + const contractId = getContractPublicId(firstContract); const { data: disciplines = [] } = useDisciplines(contractId); const selectedProjectName = diff --git a/frontend/app/(admin)/admin/doc-control/numbering/page.tsx b/frontend/app/(admin)/admin/doc-control/numbering/page.tsx index ba81c74..b36834c 100644 --- a/frontend/app/(admin)/admin/doc-control/numbering/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/numbering/page.tsx @@ -28,6 +28,7 @@ import { AuditLogsTable } from '@/components/numbering/audit-logs-table'; import { VoidReplaceForm } from '@/components/numbering/void-replace-form'; import { CancelNumberForm } from '@/components/numbering/cancel-number-form'; import { BulkImportForm } from '@/components/numbering/bulk-import-form'; +import { Contract, getContractPublicId } from '@/types/contract'; export default function NumberingPage() { const { data: projects = [] } = useProjects(); @@ -54,9 +55,9 @@ export default function NumberingPage() { // Master Data const { data: correspondenceTypes = [] } = useCorrespondenceTypes(); const { data: contractsData } = useContracts(selectedProjectId); - const contracts = Array.isArray(contractsData) ? contractsData : []; - const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined; - const contractId = firstContract?.publicId ?? firstContract?.id; + const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[]; + const firstContract = contracts[0]; + const contractId = getContractPublicId(firstContract); const { data: disciplines = [] } = useDisciplines(contractId); const { data: templateResponse, isLoading: _isLoadingTemplates } = useTemplates(); diff --git a/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx b/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx index c510c1e..3f9dff9 100644 --- a/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/reference/disciplines/page.tsx @@ -7,13 +7,14 @@ import { ColumnDef } from '@tanstack/react-table'; import { Discipline } from '@/types/master-data'; import { useState } from 'react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Contract, getContractPublicId } from '@/types/contract'; export default function DisciplinesPage() { const [selectedContractId, setSelectedContractId] = useState(null); const { data: contractsData = [] } = useContracts(); // Ensure we consistently use an array - const contracts = Array.isArray(contractsData) ? contractsData : []; + const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[]; const columns: ColumnDef[] = [ { @@ -56,10 +57,20 @@ export default function DisciplinesPage() { }, ]; - const contractOptions = contracts.map((c: { id?: number; publicId?: string; contractCode: string; contractName: string }) => ({ - label: `${c.contractName} (${c.contractCode})`, - value: String(c.publicId ?? c.id ?? ''), - })); + const contractOptions = contracts + .map((c) => { + 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 (
@@ -84,7 +95,7 @@ export default function DisciplinesPage() { data as unknown as Parameters[0] ) } - updateFn={(_id, _data) => Promise.reject('Not implemented yet')} + updateFn={(id, data) => masterDataService.updateDiscipline(id, data)} deleteFn={(id) => masterDataService.deleteDiscipline(id)} columns={columns} filters={ @@ -98,11 +109,19 @@ export default function DisciplinesPage() { All Contracts - {contracts.map((c: { id?: number; publicId?: string; contractCode: string; contractName: string }) => ( - - {c.contractName} ({c.contractCode}) - - ))} + {contracts.map((c) => { + const contractUuid = getContractPublicId(c); + + if (!contractUuid) { + return null; + } + + return ( + + {c.contractName} ({c.contractCode}) + + ); + })}
@@ -116,19 +135,19 @@ export default function DisciplinesPage() { options: contractOptions, }, { - name: 'discipline_code', + name: 'disciplineCode', label: 'Code', type: 'text', required: true, }, { - name: 'code_name_th', + name: 'codeNameTh', label: 'Name (TH)', type: 'text', required: true, }, - { name: 'code_name_en', label: 'Name (EN)', type: 'text' }, - { name: 'is_active', label: 'Active', type: 'checkbox' }, + { name: 'codeNameEn', label: 'Name (EN)', type: 'text' }, + { name: 'isActive', label: 'Active', type: 'checkbox' }, ]} /> diff --git a/frontend/app/(admin)/admin/doc-control/reference/rfa-types/page.tsx b/frontend/app/(admin)/admin/doc-control/reference/rfa-types/page.tsx index aefad00..dc6e1dc 100644 --- a/frontend/app/(admin)/admin/doc-control/reference/rfa-types/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/reference/rfa-types/page.tsx @@ -7,13 +7,14 @@ import { ColumnDef } from '@tanstack/react-table'; import { useState } from 'react'; import { RfaType } from '@/types/master-data'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Contract, getContractPublicId } from '@/types/contract'; export default function RfaTypesPage() { const [selectedContractId, setSelectedContractId] = useState(null); const { data: contractsData = [] } = useContracts(); // Ensure we consistently use an array - const contracts = Array.isArray(contractsData) ? contractsData : []; + const contracts = (Array.isArray(contractsData) ? contractsData : []) as Contract[]; const columns: ColumnDef[] = [ { @@ -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 }) => ({ - label: `${c.contractName || c.contract_name} (${c.contractCode || c.contract_code})`, - value: String(c.publicId ?? c.id ?? ''), - })); + const contractOptions = contracts + .map((c) => { + 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 (
@@ -99,11 +110,19 @@ export default function RfaTypesPage() { All Contracts - {contracts.map((c: { id?: number; publicId?: string; contract_name?: string; contract_code?: string; contractName?: string; contractCode?: string }) => ( - - {c.contractName || c.contract_name} ({c.contractCode || c.contract_code}) - - ))} + {contracts.map((c) => { + const contractUuid = getContractPublicId(c); + + if (!contractUuid) { + return null; + } + + return ( + + {c.contractName} ({c.contractCode}) + + ); + })}
diff --git a/frontend/components/numbering/template-tester.tsx b/frontend/components/numbering/template-tester.tsx index aaae301..e92512d 100644 --- a/frontend/components/numbering/template-tester.tsx +++ b/frontend/components/numbering/template-tester.tsx @@ -12,6 +12,7 @@ import { Loader2 } from 'lucide-react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { useOrganizations, useCorrespondenceTypes, useDisciplines, useContracts } from '@/hooks/use-master-data'; import { Organization } from '@/types/organization'; +import { Contract, getContractPublicId } from '@/types/contract'; // Local interfaces for Master Data since centralized ones are missing/fragmented 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 { data: organizations } = useOrganizations({ isActive: true }); 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 - const contractId = contracts?.[0]?.id; + const contractId = getContractPublicId(contracts[0]); const { data: disciplines } = useDisciplines(contractId); const handleGenerate = async () => { diff --git a/frontend/components/rfas/form.tsx b/frontend/components/rfas/form.tsx index eb8ff7f..936c2c5 100644 --- a/frontend/components/rfas/form.tsx +++ b/frontend/components/rfas/form.tsx @@ -20,6 +20,7 @@ import { useProjects } from '@/hooks/use-projects'; import { CreateRfaDto } from '@/types/dto/rfa/rfa.dto'; import { useState, useEffect, type FormEvent } from 'react'; import { correspondenceService } from '@/lib/services/correspondence.service'; +import { Contract, getContractPublicId } from '@/types/contract'; const rfaSchema = z.object({ projectId: z.string().min(1, 'Project is required'), // ADR-019: UUID @@ -47,8 +48,9 @@ type ProjectOption = { }; type ContractOption = { + publicId?: string; uuid?: string; - id?: number; + id?: string; contractName?: string; name?: string; contractCode?: string; @@ -182,8 +184,8 @@ export function RFAForm() { const selectedProjectId = watch('projectId'); const { data: contractsData, isLoading: isLoadingContracts } = useContracts(selectedProjectId); const contracts = dedupeByKey( - extractArrayData(contractsData), - (contract) => contract.uuid ?? contract.id + extractArrayData(contractsData), + (contract) => contract.publicId ?? contract.uuid ?? contract.id ); const selectedContractId = watch('contractId'); @@ -415,7 +417,7 @@ export function RFAForm() { {contracts.map((c) => { - const contractValue = getOptionValue(c.uuid ?? c.id); + const contractValue = getOptionValue(getContractPublicId(c) || c.uuid); if (!contractValue) { return null; diff --git a/frontend/hooks/use-master-data.ts b/frontend/hooks/use-master-data.ts index 70924ec..2474488 100644 --- a/frontend/hooks/use-master-data.ts +++ b/frontend/hooks/use-master-data.ts @@ -10,6 +10,17 @@ import { AxiosError } from 'axios'; import { organizationService } from '@/lib/services/organization.service'; import { projectService } from '@/lib/services/project.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 = (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 = { all: ['masterData'] as const, @@ -84,12 +95,16 @@ export function useDisciplines(contractId?: number | string) { export function useProjects(isActive: boolean = true) { return useQuery({ 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) { - return useQuery({ + return useQuery({ queryKey: ['contracts', projectId ?? 'all'], queryFn: () => contractService.getAll(projectId ? { projectId } : undefined), }); diff --git a/frontend/lib/services/contract.service.ts b/frontend/lib/services/contract.service.ts index 94b90f9..9a78cc1 100644 --- a/frontend/lib/services/contract.service.ts +++ b/frontend/lib/services/contract.service.ts @@ -1,5 +1,30 @@ import apiClient from '@/lib/api/client'; 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 = { /** @@ -8,10 +33,7 @@ export const contractService = { */ getAll: async (params?: SearchContractDto) => { const response = await apiClient.get('/contracts', { params }); - if (response.data && Array.isArray(response.data.data)) { - return response.data.data; - } - return response.data.data || response.data; + return extractContractArray(response.data?.data ?? response.data); }, /** @@ -20,7 +42,8 @@ export const contractService = { */ getByUuid: async (uuid: string) => { const response = await apiClient.get(`/contracts/${uuid}`); - return response.data; + const payload = response.data?.data ?? response.data; + return normalizeContract(payload as Contract); }, /** diff --git a/frontend/lib/services/master-data.service.ts b/frontend/lib/services/master-data.service.ts index 782c555..0bf6ac3 100644 --- a/frontend/lib/services/master-data.service.ts +++ b/frontend/lib/services/master-data.service.ts @@ -141,6 +141,12 @@ export const masterDataService = { return response.data; }, + /** แก้ไขสาขางาน */ + updateDiscipline: async (id: number, data: Partial) => { + const response = await apiClient.patch(`/master/disciplines/${id}`, data); + return response.data; + }, + // --- Sub-Types Management (Admin / Req 6B) --- /** ดึงรายชื่อประเภทย่อย (กรองตาม Contract และ Type) */ diff --git a/frontend/types/contract.ts b/frontend/types/contract.ts new file mode 100644 index 0000000..98f464b --- /dev/null +++ b/frontend/types/contract.ts @@ -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): string => + String(contract?.publicId ?? contract?.id ?? ''); + +export const getProjectPublicId = ( + project?: Pick +): string => String(project?.publicId ?? project?.id ?? '');