This commit is contained in:
+41
-65
@@ -16,15 +16,15 @@ You value **Data Integrity**, **Security**, and **Clean Architecture**.
|
|||||||
|
|
||||||
### 📊 Project Status: UAT Ready (2026-03-11)
|
### 📊 Project Status: UAT Ready (2026-03-11)
|
||||||
|
|
||||||
| Area | Status | Notes |
|
| Area | Status | Notes |
|
||||||
|------|--------|-------|
|
| ------------- | ------------------------ | ------------------------------------ |
|
||||||
| Backend | ✅ Production Ready | 18 Modules, ADR-018 AI Isolation |
|
| Backend | ✅ Production Ready | 18 Modules, ADR-018 AI Isolation |
|
||||||
| Frontend | ✅ 100% Complete | App Router, TanStack Query, Zustand |
|
| Frontend | ✅ 100% Complete | App Router, TanStack Query, Zustand |
|
||||||
| 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 | Blue-Green, QNAP Container Station |
|
| Deployment | 📋 Pending Go-Live | Blue-Green, QNAP Container Station |
|
||||||
|
|
||||||
- **Goal:** Manage construction documents (Correspondence, RFA, Contract Drawings, Shop Drawings)
|
- **Goal:** Manage construction documents (Correspondence, RFA, Contract Drawings, Shop Drawings)
|
||||||
with complex multi-level approval workflows.
|
with complex multi-level approval workflows.
|
||||||
@@ -54,71 +54,47 @@ You value **Data Integrity**, **Security**, and **Clean Architecture**.
|
|||||||
6. **Rate Limiting:** Apply ThrottlerGuard on auth endpoints.
|
6. **Rate Limiting:** Apply ThrottlerGuard on auth endpoints.
|
||||||
7. **AI Isolation (ADR-018):** Ollama MUST run on Admin Desktop only (NOT on QNAP/production server). AI has NO direct DB access, NO write access to uploads. Output JSON only.
|
7. **AI Isolation (ADR-018):** Ollama MUST run on Admin Desktop only (NOT on QNAP/production server). AI has NO direct DB access, NO write access to uploads. Output JSON only.
|
||||||
|
|
||||||
## 📋 Workflow & Spec Guidelines
|
## 📋 Spec Guidelines
|
||||||
|
|
||||||
- Always follow specs in `specs/` (v1.8.1). Priority: `06-Decision-Records` > `05-Engineering-Guidelines` > others.
|
- Always follow specs in `specs/` (v1.8.1). Priority: `06-Decision-Records` > `05-Engineering-Guidelines` > others.
|
||||||
- Always verify database schema against **`specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`** before writing queries. (Schema split: `01-drop`, `02-tables`, `03-views-indexes`)
|
- Always verify database schema against **`specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`** before writing queries. (Schema split: `01-drop`, `02-tables`, `03-views-indexes`)
|
||||||
- Check data dictionary at **`specs/03-Data-and-Storage/03-01-data-dictionary.md`** for field meanings and business rules.
|
- Check data dictionary at **`specs/03-Data-and-Storage/03-01-data-dictionary.md`** for field meanings and business rules.
|
||||||
- Check seed data: **`lcbp3-v1.8.0-seed-basic.sql`** (reference data), **`lcbp3-v1.8.0-seed-permissions.sql`** (CASL permissions).
|
|
||||||
- For migration context: **`specs/03-Data-and-Storage/03-04-legacy-data-migration.md`** and **`03-05-n8n-migration-setup-guide.md`**.
|
|
||||||
|
|
||||||
### 📁 Key Spec Documents (Quick Reference)
|
### 📁 Key Spec Documents (Quick Reference)
|
||||||
|
|
||||||
| เอกสาร | Path | ใช้เมื่อ |
|
| เอกสาร | Path | ใช้เมื่อ |
|
||||||
|--------|------|--------|
|
| -------------------- | ----------------------------------------------------------- | ----------------------------------- |
|
||||||
| **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** | `01-Requirements/01-06-edge-cases-and-rules.md` | 37 Rules ป้องกัน Bug |
|
| **Edge Cases** | `01-Requirements/01-06-edge-cases-and-rules.md` | 37 Rules ป้องกัน Bug |
|
||||||
| **Migration Scope** | `03-Data-and-Storage/03-06-migration-business-scope.md` | งาน Migration Bot |
|
| **Migration Scope** | `03-Data-and-Storage/03-06-migration-business-scope.md` | งาน Migration Bot |
|
||||||
| **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 |
|
||||||
| **ADR-009** | `06-Decision-Records/ADR-009-db-strategy.md` | Schema Change Process |
|
| **ADR-009** | `06-Decision-Records/ADR-009-db-strategy.md` | Schema Change Process |
|
||||||
| **ADR-018** | `06-Decision-Records/ADR-018-ai-boundary.md` | AI/Ollama Integration Rules |
|
| **ADR-018** | `06-Decision-Records/ADR-018-ai-boundary.md` | AI/Ollama Integration Rules |
|
||||||
|
| **ADR-019** | `06-Decision-Records/ADR-019-hybrid-identifier-strategy.md` | Hybrid ID Strategy (INT + UUIDv7) |
|
||||||
|
|
||||||
### ADR Reference (All 17 + Patch)
|
### ADR Reference (All 17 + Patch + ADR-019)
|
||||||
|
|
||||||
Adhere to all ADRs in `specs/06-Decision-Records/`:
|
| ADR | Topic | Key Decision |
|
||||||
|
| ------- | -------------------------- | -------------------------------------------------- |
|
||||||
| ADR | Topic | Key Decision |
|
| ADR-001 | Workflow Engine | Unified state machine for document workflows |
|
||||||
| ------- | ------------------------- | -------------------------------------------------- |
|
| ADR-002 | Doc Numbering | Redis Redlock + DB optimistic locking |
|
||||||
| ADR-001 | Workflow Engine | Unified state machine for document workflows |
|
| ADR-005 | Technology Stack | NestJS + Next.js + MariaDB + Redis |
|
||||||
| ADR-002 | Doc Numbering | Redis Redlock + DB optimistic locking |
|
| ADR-006 | Redis Caching | Cache strategy and invalidation patterns |
|
||||||
| ADR-005 | Technology Stack | NestJS + Next.js + MariaDB + Redis |
|
| ADR-008 | Email Notification | BullMQ queue-based email/LINE/in-app |
|
||||||
| ADR-006 | Redis Caching | Cache strategy and invalidation patterns |
|
| ADR-009 | DB Strategy | No TypeORM migrations — modify schema SQL directly |
|
||||||
| ADR-008 | Email Notification | BullMQ queue-based email/LINE/in-app |
|
| ADR-010 | Logging/Monitoring | Prometheus + Loki + Grafana stack |
|
||||||
| ADR-009 | DB Strategy | No TypeORM migrations — modify schema SQL directly |
|
| ADR-011 | App Router | Next.js App Router with RSC patterns |
|
||||||
| ADR-010 | Logging/Monitoring | Prometheus + Loki + Grafana stack |
|
| ADR-012 | UI Components | Shadcn/UI component library |
|
||||||
| ADR-011 | App Router | Next.js App Router with RSC patterns |
|
| ADR-013 | Form Handling | React Hook Form + Zod validation |
|
||||||
| ADR-012 | UI Components | Shadcn/UI component library |
|
| ADR-014 | State Management | TanStack Query (server) + Zustand (client) |
|
||||||
| ADR-013 | Form Handling | React Hook Form + Zod validation |
|
| ADR-015 | Deployment | Docker Compose + Gitea CI/CD |
|
||||||
| ADR-014 | State Management | TanStack Query (server) + Zustand (client) |
|
| ADR-016 | Security | JWT + CASL RBAC + Helmet.js + ClamAV |
|
||||||
| ADR-015 | Deployment | Docker Compose + Gitea CI/CD |
|
| ADR-017 | Ollama Migration | Local AI + n8n for legacy data import |
|
||||||
| ADR-016 | Security | JWT + CASL RBAC + Helmet.js + ClamAV |
|
| ADR-018 | AI Boundary (Patch 1.8.1) | AI isolation — no direct DB/storage access |
|
||||||
| ADR-017 | Ollama Migration | Local AI + n8n for legacy data import |
|
| ADR-019 | Hybrid Identifier Strategy | INT PK (internal) + UUIDv7 BINARY(16) (public API) |
|
||||||
| ADR-018 | AI Boundary (Patch 1.8.1) | AI isolation — no direct DB/storage access |
|
|
||||||
|
|
||||||
## 🎯 Active Skills
|
|
||||||
|
|
||||||
- **`nestjs-best-practices`** — Apply when writing/reviewing any NestJS code (modules, services, controllers, guards, interceptors, DTOs)
|
|
||||||
- **`next-best-practices`** — Apply when writing/reviewing any Next.js code (App Router, RSC boundaries, async patterns, data fetching, error handling)
|
|
||||||
- **`speckit.security-audit`** — Apply when auditing security (OWASP Top 10, CASL, ClamAV, LCBP3-specific checks)
|
|
||||||
|
|
||||||
## 🔄 Speckit Workflow Pipeline
|
|
||||||
|
|
||||||
Use `/slash-command` to trigger these workflows. Always prefer spec-driven development for new features.
|
|
||||||
|
|
||||||
| Phase | Command | เมื่อใช้ |
|
|
||||||
| -------------------- | ---------------------------------------------------------- | ----------------------------------------------------- |
|
|
||||||
| **Full Pipeline** | `/speckit.all` | Feature ใหม่ — รัน Specify→...→Validate (10 steps) |
|
|
||||||
| **Feature Design** | `/speckit.prepare` | Preparation only — Specify→Clarify→Plan→Tasks→Analyze |
|
|
||||||
| **Implement** | `/07-speckit.implement` | เขียนโค้ดตาม tasks.md พร้อม anti-regression |
|
|
||||||
| **QA** | `/08-speckit.checker` | ตรวจ TypeScript + ESLint + Security |
|
|
||||||
| **Test** | `/09-speckit.tester` | รัน Jest/Vitest + coverage report |
|
|
||||||
| **Review** | `/10-speckit.reviewer` | Code review — Logic, Performance, Style |
|
|
||||||
| **Validate** | `/11-speckit.validate` | ยืนยันว่า implementation ตรงกับ spec.md |
|
|
||||||
| **Schema Change** | `/schema-change` | แก้ schema SQL → data dictionary → notify user |
|
|
||||||
| **Project-Specific** | `/create-backend-module` `/create-frontend-page` `/deploy` | งานประจำของ LCBP3-DMS |
|
|
||||||
|
|
||||||
## 🚫 Forbidden Actions
|
## 🚫 Forbidden Actions
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ You value **Data Integrity**, **Security**, and **Clean Architecture**.
|
|||||||
|
|
||||||
### 📊 Project Status: UAT Ready (2026-03-11)
|
### 📊 Project Status: UAT Ready (2026-03-11)
|
||||||
|
|
||||||
| Area | Status | Notes |
|
| Area | Status | Notes |
|
||||||
|------|--------|-------|
|
| ------------- | ------------------------ | ------------------------------------ |
|
||||||
| Backend | ✅ Production Ready | 18 Modules, ADR-018 AI Isolation |
|
| Backend | ✅ Production Ready | 18 Modules, ADR-018 AI Isolation |
|
||||||
| Frontend | ✅ 100% Complete | App Router, TanStack Query, Zustand |
|
| Frontend | ✅ 100% Complete | App Router, TanStack Query, Zustand |
|
||||||
| 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 | Blue-Green, QNAP Container Station |
|
| Deployment | 📋 Pending Go-Live | Blue-Green, QNAP Container Station |
|
||||||
|
|
||||||
- **Goal:** Manage construction documents (Correspondence, RFA, Contract Drawings, Shop Drawings)
|
- **Goal:** Manage construction documents (Correspondence, RFA, Contract Drawings, Shop Drawings)
|
||||||
with complex multi-level approval workflows.
|
with complex multi-level approval workflows.
|
||||||
@@ -60,37 +60,39 @@ You value **Data Integrity**, **Security**, and **Clean Architecture**.
|
|||||||
|
|
||||||
### 📁 Key Spec Documents (Quick Reference)
|
### 📁 Key Spec Documents (Quick Reference)
|
||||||
|
|
||||||
| เอกสาร | Path | ใช้เมื่อ |
|
| เอกสาร | Path | ใช้เมื่อ |
|
||||||
|--------|------|--------|
|
| -------------------- | ----------------------------------------------------------- | ----------------------------------- |
|
||||||
| **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** | `01-Requirements/01-06-edge-cases-and-rules.md` | 37 Rules ป้องกัน Bug |
|
| **Edge Cases** | `01-Requirements/01-06-edge-cases-and-rules.md` | 37 Rules ป้องกัน Bug |
|
||||||
| **Migration Scope** | `03-Data-and-Storage/03-06-migration-business-scope.md` | งาน Migration Bot |
|
| **Migration Scope** | `03-Data-and-Storage/03-06-migration-business-scope.md` | งาน Migration Bot |
|
||||||
| **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 |
|
||||||
| **ADR-009** | `06-Decision-Records/ADR-009-db-strategy.md` | Schema Change Process |
|
| **ADR-009** | `06-Decision-Records/ADR-009-db-strategy.md` | Schema Change Process |
|
||||||
| **ADR-018** | `06-Decision-Records/ADR-018-ai-boundary.md` | AI/Ollama Integration Rules |
|
| **ADR-018** | `06-Decision-Records/ADR-018-ai-boundary.md` | AI/Ollama Integration Rules |
|
||||||
|
| **ADR-019** | `06-Decision-Records/ADR-019-hybrid-identifier-strategy.md` | Hybrid ID Strategy (INT + UUIDv7) |
|
||||||
|
|
||||||
### ADR Reference (All 17 + Patch)
|
### ADR Reference (All 17 + Patch + ADR-019)
|
||||||
|
|
||||||
| ADR | Topic | Key Decision |
|
| ADR | Topic | Key Decision |
|
||||||
| ------- | ------------------------- | -------------------------------------------------- |
|
| ------- | -------------------------- | -------------------------------------------------- |
|
||||||
| ADR-001 | Workflow Engine | Unified state machine for document workflows |
|
| ADR-001 | Workflow Engine | Unified state machine for document workflows |
|
||||||
| ADR-002 | Doc Numbering | Redis Redlock + DB optimistic locking |
|
| ADR-002 | Doc Numbering | Redis Redlock + DB optimistic locking |
|
||||||
| ADR-005 | Technology Stack | NestJS + Next.js + MariaDB + Redis |
|
| ADR-005 | Technology Stack | NestJS + Next.js + MariaDB + Redis |
|
||||||
| ADR-006 | Redis Caching | Cache strategy and invalidation patterns |
|
| ADR-006 | Redis Caching | Cache strategy and invalidation patterns |
|
||||||
| ADR-008 | Email Notification | BullMQ queue-based email/LINE/in-app |
|
| ADR-008 | Email Notification | BullMQ queue-based email/LINE/in-app |
|
||||||
| ADR-009 | DB Strategy | No TypeORM migrations — modify schema SQL directly |
|
| ADR-009 | DB Strategy | No TypeORM migrations — modify schema SQL directly |
|
||||||
| ADR-010 | Logging/Monitoring | Prometheus + Loki + Grafana stack |
|
| ADR-010 | Logging/Monitoring | Prometheus + Loki + Grafana stack |
|
||||||
| ADR-011 | App Router | Next.js App Router with RSC patterns |
|
| ADR-011 | App Router | Next.js App Router with RSC patterns |
|
||||||
| ADR-012 | UI Components | Shadcn/UI component library |
|
| ADR-012 | UI Components | Shadcn/UI component library |
|
||||||
| ADR-013 | Form Handling | React Hook Form + Zod validation |
|
| ADR-013 | Form Handling | React Hook Form + Zod validation |
|
||||||
| ADR-014 | State Management | TanStack Query (server) + Zustand (client) |
|
| ADR-014 | State Management | TanStack Query (server) + Zustand (client) |
|
||||||
| ADR-015 | Deployment | Docker Compose + Gitea CI/CD |
|
| ADR-015 | Deployment | Docker Compose + Gitea CI/CD |
|
||||||
| ADR-016 | Security | JWT + CASL RBAC + Helmet.js + ClamAV |
|
| ADR-016 | Security | JWT + CASL RBAC + Helmet.js + ClamAV |
|
||||||
| ADR-017 | Ollama Migration | Local AI + n8n for legacy data import |
|
| ADR-017 | Ollama Migration | Local AI + n8n for legacy data import |
|
||||||
| ADR-018 | AI Boundary (Patch 1.8.1) | AI isolation — no direct DB/storage access |
|
| ADR-018 | AI Boundary (Patch 1.8.1) | AI isolation — no direct DB/storage access |
|
||||||
|
| ADR-019 | Hybrid Identifier Strategy | INT PK (internal) + UUIDv7 BINARY(16) (public API) |
|
||||||
|
|
||||||
## 🚫 Forbidden Actions
|
## 🚫 Forbidden Actions
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,19 @@ You value **Data Integrity**, **Security**, and **Clean Architecture**.
|
|||||||
|
|
||||||
## 🏗️ Project Overview
|
## 🏗️ Project Overview
|
||||||
|
|
||||||
**LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)** — Version 1.8.0 (Patch 1.8.1)
|
**LCBP3-DMS (Laem Chabang Port Phase 3 - Document Management System)** — Version 1.8.1 (Patch)
|
||||||
|
|
||||||
|
### 📊 Project Status: UAT Ready (2026-03-11)
|
||||||
|
|
||||||
|
| Area | Status | Notes |
|
||||||
|
| ------------- | ------------------------ | ------------------------------------ |
|
||||||
|
| Backend | ✅ Production Ready | 18 Modules, ADR-018 AI Isolation |
|
||||||
|
| Frontend | ✅ 100% Complete | App Router, TanStack Query, Zustand |
|
||||||
|
| 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 | Blue-Green, QNAP Container Station |
|
||||||
|
|
||||||
- **Goal:** Manage construction documents (Correspondence, RFA, Contract Drawings, Shop Drawings)
|
- **Goal:** Manage construction documents (Correspondence, RFA, Contract Drawings, Shop Drawings)
|
||||||
with complex multi-level approval workflows.
|
with complex multi-level approval workflows.
|
||||||
@@ -38,43 +50,59 @@ You value **Data Integrity**, **Security**, and **Clean Architecture**.
|
|||||||
6. **Rate Limiting:** Apply ThrottlerGuard on auth endpoints.
|
6. **Rate Limiting:** Apply ThrottlerGuard on auth endpoints.
|
||||||
7. **AI Isolation (ADR-018):** Ollama MUST run on Admin Desktop only (NOT on QNAP/production server). AI has NO direct DB access, NO write access to uploads. Output JSON only.
|
7. **AI Isolation (ADR-018):** Ollama MUST run on Admin Desktop only (NOT on QNAP/production server). AI has NO direct DB access, NO write access to uploads. Output JSON only.
|
||||||
|
|
||||||
## 📋 Workflow & Spec Guidelines
|
## 📋 Spec Guidelines
|
||||||
|
|
||||||
- Always follow specs in `specs/` (v1.8.0). Priority: `06-Decision-Records` > `05-Engineering-Guidelines` > others.
|
- Always follow specs in `specs/` (v1.8.1). Priority: `06-Decision-Records` > `05-Engineering-Guidelines` > others.
|
||||||
- Always verify database schema against **`specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`** before writing queries. (Schema split: `01-drop`, `02-tables`, `03-views-indexes`)
|
- Always verify database schema against **`specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`** before writing queries. (Schema split: `01-drop`, `02-tables`, `03-views-indexes`)
|
||||||
- Check data dictionary at **`specs/03-Data-and-Storage/03-01-data-dictionary.md`** for field meanings and business rules.
|
- Check data dictionary at **`specs/03-Data-and-Storage/03-01-data-dictionary.md`** for field meanings and business rules.
|
||||||
- Check seed data: **`lcbp3-v1.8.0-seed-basic.sql`** (reference data), **`lcbp3-v1.8.0-seed-permissions.sql`** (CASL permissions).
|
|
||||||
- For migration context: **`specs/03-Data-and-Storage/03-04-legacy-data-migration.md`** and **`03-05-n8n-migration-setup-guide.md`**.
|
|
||||||
|
|
||||||
### ADR Reference (All 17 + Patch)
|
### 📁 Key Spec Documents (Quick Reference)
|
||||||
|
|
||||||
Adhere to all ADRs in `specs/06-Decision-Records/`:
|
| เอกสาร | Path | ใช้เมื่อ |
|
||||||
|
| -------------------- | ----------------------------------------------------------- | ----------------------------------- |
|
||||||
|
| **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** | `01-Requirements/01-06-edge-cases-and-rules.md` | 37 Rules ป้องกัน Bug |
|
||||||
|
| **Migration Scope** | `03-Data-and-Storage/03-06-migration-business-scope.md` | งาน Migration Bot |
|
||||||
|
| **Release Policy** | `04-Infrastructure-OPS/04-08-release-management-policy.md` | ก่อน Deploy / Hotfix |
|
||||||
|
| **UAT Criteria** | `01-Requirements/01-05-acceptance-criteria.md` | ตรวจความสมบูรณ์ Feature |
|
||||||
|
| **ADR-009** | `06-Decision-Records/ADR-009-db-strategy.md` | Schema Change Process |
|
||||||
|
| **ADR-018** | `06-Decision-Records/ADR-018-ai-boundary.md` | AI/Ollama Integration Rules |
|
||||||
|
| **ADR-019** | `06-Decision-Records/ADR-019-hybrid-identifier-strategy.md` | Hybrid ID Strategy (INT + UUIDv7) |
|
||||||
|
|
||||||
| ADR | Topic | Key Decision |
|
### ADR Reference (All 17 + Patch + ADR-019)
|
||||||
| ------- | ------------------------- | -------------------------------------------------- |
|
|
||||||
| ADR-001 | Workflow Engine | Unified state machine for document workflows |
|
| ADR | Topic | Key Decision |
|
||||||
| ADR-002 | Doc Numbering | Redis Redlock + DB optimistic locking |
|
| ------- | -------------------------- | -------------------------------------------------- |
|
||||||
| ADR-005 | Technology Stack | NestJS + Next.js + MariaDB + Redis |
|
| ADR-001 | Workflow Engine | Unified state machine for document workflows |
|
||||||
| ADR-006 | Redis Caching | Cache strategy and invalidation patterns |
|
| ADR-002 | Doc Numbering | Redis Redlock + DB optimistic locking |
|
||||||
| ADR-008 | Email Notification | BullMQ queue-based email/LINE/in-app |
|
| ADR-005 | Technology Stack | NestJS + Next.js + MariaDB + Redis |
|
||||||
| ADR-009 | DB Strategy | No TypeORM migrations — modify schema SQL directly |
|
| ADR-006 | Redis Caching | Cache strategy and invalidation patterns |
|
||||||
| ADR-010 | Logging/Monitoring | Prometheus + Loki + Grafana stack |
|
| ADR-008 | Email Notification | BullMQ queue-based email/LINE/in-app |
|
||||||
| ADR-011 | App Router | Next.js App Router with RSC patterns |
|
| ADR-009 | DB Strategy | No TypeORM migrations — modify schema SQL directly |
|
||||||
| ADR-012 | UI Components | Shadcn/UI component library |
|
| ADR-010 | Logging/Monitoring | Prometheus + Loki + Grafana stack |
|
||||||
| ADR-013 | Form Handling | React Hook Form + Zod validation |
|
| ADR-011 | App Router | Next.js App Router with RSC patterns |
|
||||||
| ADR-014 | State Management | TanStack Query (server) + Zustand (client) |
|
| ADR-012 | UI Components | Shadcn/UI component library |
|
||||||
| ADR-015 | Deployment | Docker Compose + Gitea CI/CD |
|
| ADR-013 | Form Handling | React Hook Form + Zod validation |
|
||||||
| ADR-016 | Security | JWT + CASL RBAC + Helmet.js + ClamAV |
|
| ADR-014 | State Management | TanStack Query (server) + Zustand (client) |
|
||||||
| ADR-017 | Ollama Migration | Local AI + n8n for legacy data import |
|
| ADR-015 | Deployment | Docker Compose + Gitea CI/CD |
|
||||||
| ADR-018 | AI Boundary (Patch 1.8.1) | AI isolation — no direct DB/storage access |
|
| ADR-016 | Security | JWT + CASL RBAC + Helmet.js + ClamAV |
|
||||||
|
| ADR-017 | Ollama Migration | Local AI + n8n for legacy data import |
|
||||||
|
| ADR-018 | AI Boundary (Patch 1.8.1) | AI isolation — no direct DB/storage access |
|
||||||
|
| ADR-019 | Hybrid Identifier Strategy | INT PK (internal) + UUIDv7 BINARY(16) (public API) |
|
||||||
|
|
||||||
## 🚫 Forbidden Actions
|
## 🚫 Forbidden Actions
|
||||||
|
|
||||||
- DO NOT use SQL Triggers (Business logic must be in NestJS services).
|
- DO NOT use SQL Triggers (Business logic must be in NestJS services).
|
||||||
- DO NOT use `.env` files for production deployment — QNAP Container Station requires secrets directly in `docker-compose.yml` environment section.
|
- DO NOT use `.env` files for production deployment — QNAP Container Station requires secrets directly in `docker-compose.yml` environment section.
|
||||||
- DO NOT run database migrations — modify the schema SQL file directly.
|
- DO NOT run database migrations — modify the schema SQL file directly (ADR-009).
|
||||||
- DO NOT invent table names or columns — use ONLY what is defined in the schema SQL file.
|
- DO NOT invent table names or columns — use ONLY what is defined in the schema SQL file.
|
||||||
- DO NOT generate code that violates OWASP Top 10 security practices.
|
- DO NOT generate code that violates OWASP Top 10 security practices.
|
||||||
- DO NOT use `any` TypeScript type anywhere.
|
- DO NOT use `any` TypeScript type anywhere.
|
||||||
- DO NOT let AI (Ollama) access production database directly — all writes go through DMS API.
|
- DO NOT let AI (Ollama) access production database directly — all writes go through DMS API (ADR-018).
|
||||||
- DO NOT bypass StorageService for file operations — all file moves must go through the API.
|
- DO NOT bypass StorageService for file operations — all file moves must go through the API.
|
||||||
|
- DO NOT deploy to Production without completing Release Gates — see `04-08-release-management-policy.md`.
|
||||||
|
- DO NOT start Legacy Migration without Go/No-Go Gate #1 approval — see `03-06-migration-business-scope.md`.
|
||||||
|
- DO NOT modify Migration Bot Token scope — IP Whitelist + 7-day Expiry + REVOKE after migration.
|
||||||
|
- DO NOT close UAT sign-off without all Acceptance Criteria ✅ — see `01-05-acceptance-criteria.md`.
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^11.1.0",
|
||||||
"winston": "^3.18.3",
|
"winston": "^3.18.3",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
"@types/opossum": "^8.1.9",
|
"@types/opossum": "^8.1.9",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^10.0.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-prettier": "^5.2.2",
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Column, BeforeInsert } from 'typeorm';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base entity providing a UUID public identifier column.
|
||||||
|
* Uses MariaDB native UUID type (stored as BINARY(16) internally,
|
||||||
|
* auto-converts to string format — no transformer needed).
|
||||||
|
*
|
||||||
|
* App generates UUIDv7 via @BeforeInsert(); DB DEFAULT UUID() is fallback.
|
||||||
|
*
|
||||||
|
* @see ADR-019 Hybrid Identifier Strategy
|
||||||
|
*/
|
||||||
|
export abstract class UuidBaseEntity {
|
||||||
|
@Column({
|
||||||
|
type: 'uuid',
|
||||||
|
unique: true,
|
||||||
|
nullable: false,
|
||||||
|
comment: 'UUID Public Identifier (ADR-019)',
|
||||||
|
})
|
||||||
|
uuid!: string;
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
generateUuid(): void {
|
||||||
|
if (!this.uuid) {
|
||||||
|
this.uuid = uuidv7();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,13 @@ import {
|
|||||||
JoinColumn,
|
JoinColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from '../../../modules/user/entities/user.entity';
|
import { User } from '../../../modules/user/entities/user.entity';
|
||||||
|
import { UuidBaseEntity } from '../../entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('attachments')
|
@Entity('attachments')
|
||||||
export class Attachment {
|
export class Attachment extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ name: 'original_filename', length: 255 })
|
@Column({ name: 'original_filename', length: 255 })
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
import { instanceToPlain } from 'class-transformer';
|
||||||
|
|
||||||
/** Metadata สำหรับ Paginated Response */
|
/** Metadata สำหรับ Paginated Response */
|
||||||
export interface ResponseMeta {
|
export interface ResponseMeta {
|
||||||
@@ -53,24 +54,31 @@ export class TransformInterceptor<T>
|
|||||||
): Observable<ApiResponse<T>> {
|
): Observable<ApiResponse<T>> {
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
map((data: T) => {
|
map((data: T) => {
|
||||||
const response = context.switchToHttp().getResponse<{ statusCode: number }>();
|
const response = context
|
||||||
|
.switchToHttp()
|
||||||
|
.getResponse<{ statusCode: number }>();
|
||||||
|
|
||||||
|
// ADR-019: Serialize entities via class-transformer
|
||||||
|
// This applies @Exclude() decorators to strip internal INT ids from responses
|
||||||
|
const serialized = instanceToPlain(data) as T;
|
||||||
|
|
||||||
// Handle Pagination Response (Standardize)
|
// Handle Pagination Response (Standardize)
|
||||||
// ถ้า data มี structure { data: [], meta: {} } ให้ unzip ออกมา
|
// ถ้า data มี structure { data: [], meta: {} } ให้ unzip ออกมา
|
||||||
if (isPaginatedPayload(data)) {
|
if (isPaginatedPayload(serialized)) {
|
||||||
return {
|
return {
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
message: data.message ?? 'Success',
|
message: serialized.message ?? 'Success',
|
||||||
data: data.data as unknown as T,
|
data: serialized.data as unknown as T,
|
||||||
meta: data.meta,
|
meta: serialized.meta,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataAsRecord = data as Record<string, unknown>;
|
const dataAsRecord = serialized as Record<string, unknown>;
|
||||||
return {
|
return {
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
message: (dataAsRecord?.['message'] as string | undefined) ?? 'Success',
|
message:
|
||||||
data: (dataAsRecord?.['result'] as T | undefined) ?? data,
|
(dataAsRecord?.['message'] as string | undefined) ?? 'Success',
|
||||||
|
data: (dataAsRecord?.['result'] as T | undefined) ?? serialized,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
|
||||||
|
import { validate as uuidValidate } from 'uuid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a route parameter is a valid UUID string.
|
||||||
|
* Accepts any UUID version (v1 from DB DEFAULT, v7 from app generation).
|
||||||
|
*
|
||||||
|
* Usage: @Param('uuid', ParseUuidPipe) uuid: string
|
||||||
|
*
|
||||||
|
* @see ADR-019 Hybrid Identifier Strategy
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ParseUuidPipe implements PipeTransform<string> {
|
||||||
|
transform(value: string): string {
|
||||||
|
if (!uuidValidate(value)) {
|
||||||
|
throw new BadRequestException(`Invalid UUID format: ${value}`);
|
||||||
|
}
|
||||||
|
return value.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import { RbacGuard } from '../../common/guards/rbac.guard';
|
|||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
|
|
||||||
@ApiTags('Circulations')
|
@ApiTags('Circulations')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -45,11 +46,11 @@ export class CirculationController {
|
|||||||
return this.circulationService.findAll(searchDto, user);
|
return this.circulationService.findAll(searchDto, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get circulation details' })
|
@ApiOperation({ summary: 'Get circulation details' })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.circulationService.findOne(id);
|
return this.circulationService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('routings/:id')
|
@Patch('routings/:id')
|
||||||
@@ -58,7 +59,7 @@ export class CirculationController {
|
|||||||
updateRouting(
|
updateRouting(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@Body() updateDto: UpdateCirculationRoutingDto,
|
@Body() updateDto: UpdateCirculationRoutingDto,
|
||||||
@CurrentUser() user: User,
|
@CurrentUser() user: User
|
||||||
) {
|
) {
|
||||||
return this.circulationService.updateRoutingStatus(id, updateDto, user);
|
return this.circulationService.updateRoutingStatus(id, updateDto, user);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,17 @@ export class CirculationService {
|
|||||||
return circulation;
|
return circulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOneByUuid(uuid: string) {
|
||||||
|
const circulation = await this.circulationRepo.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
relations: ['routings', 'routings.assignee', 'correspondence', 'creator'],
|
||||||
|
order: { routings: { stepNumber: 'ASC' } },
|
||||||
|
});
|
||||||
|
if (!circulation)
|
||||||
|
throw new NotFoundException(`Circulation UUID ${uuid} not found`);
|
||||||
|
return circulation;
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ Logic อัปเดตสถานะและปิดงาน
|
// ✅ Logic อัปเดตสถานะและปิดงาน
|
||||||
async updateRoutingStatus(
|
async updateRoutingStatus(
|
||||||
routingId: number,
|
routingId: number,
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ import { Organization } from '../../organization/entities/organization.entity';
|
|||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { CirculationStatusCode } from './circulation-status-code.entity';
|
import { CirculationStatusCode } from './circulation-status-code.entity';
|
||||||
import { CirculationRouting } from './circulation-routing.entity';
|
import { CirculationRouting } from './circulation-routing.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('circulations')
|
@Entity('circulations')
|
||||||
export class Circulation {
|
export class Circulation extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ name: 'correspondence_id', nullable: true })
|
@Column({ name: 'correspondence_id', nullable: true })
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Delete,
|
Delete,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
ParseIntPipe,
|
|
||||||
Query,
|
Query,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +21,7 @@ import { UpdateContractDto } from './dto/update-contract.dto.js';
|
|||||||
import { SearchContractDto } from './dto/search-contract.dto.js';
|
import { SearchContractDto } from './dto/search-contract.dto.js';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
|
|
||||||
@ApiTags('Contracts')
|
@ApiTags('Contracts')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -45,26 +45,26 @@ export class ContractController {
|
|||||||
return this.contractService.findAll(query);
|
return this.contractService.findAll(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get Contract by ID' })
|
@ApiOperation({ summary: 'Get Contract by UUID' })
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.contractService.findOne(id);
|
return this.contractService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':uuid')
|
||||||
@RequirePermission('master_data.manage')
|
@RequirePermission('master_data.manage')
|
||||||
@ApiOperation({ summary: 'Update Contract' })
|
@ApiOperation({ summary: 'Update Contract' })
|
||||||
update(
|
update(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() dto: UpdateContractDto
|
@Body() dto: UpdateContractDto
|
||||||
) {
|
) {
|
||||||
return this.contractService.update(id, dto);
|
return this.contractService.update(uuid, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':uuid')
|
||||||
@RequirePermission('master_data.manage')
|
@RequirePermission('master_data.manage')
|
||||||
@ApiOperation({ summary: 'Delete Contract' })
|
@ApiOperation({ summary: 'Delete Contract' })
|
||||||
remove(@Param('id', ParseIntPipe) id: number) {
|
remove(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.contractService.remove(id);
|
return this.contractService.remove(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,15 +87,24 @@ export class ContractService {
|
|||||||
return contract;
|
return contract;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: number, dto: UpdateContractDto) {
|
async findOneByUuid(uuid: string) {
|
||||||
const contract = await this.findOne(id);
|
const contract = await this.contractRepo.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
relations: ['project'],
|
||||||
|
});
|
||||||
|
if (!contract)
|
||||||
|
throw new NotFoundException(`Contract UUID ${uuid} not found`);
|
||||||
|
return contract;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(uuid: string, dto: UpdateContractDto) {
|
||||||
|
const contract = await this.findOneByUuid(uuid);
|
||||||
Object.assign(contract, dto);
|
Object.assign(contract, dto);
|
||||||
return this.contractRepo.save(contract);
|
return this.contractRepo.save(contract);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: number) {
|
async remove(uuid: string) {
|
||||||
const contract = await this.findOne(id);
|
const contract = await this.findOneByUuid(uuid);
|
||||||
// Schema doesn't have deleted_at for Contract either.
|
|
||||||
return this.contractRepo.remove(contract);
|
return this.contractRepo.remove(contract);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,34 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
|
BeforeInsert,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
import { Project } from '../../project/entities/project.entity';
|
import { Project } from '../../project/entities/project.entity';
|
||||||
|
|
||||||
@Entity('contracts')
|
@Entity('contracts')
|
||||||
export class Contract extends BaseEntity {
|
export class Contract extends BaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'uuid',
|
||||||
|
unique: true,
|
||||||
|
nullable: false,
|
||||||
|
comment: 'UUID Public Identifier (ADR-019)',
|
||||||
|
})
|
||||||
|
uuid!: string;
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
generateUuid(): void {
|
||||||
|
if (!this.uuid) {
|
||||||
|
this.uuid = uuidv7();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Column({ name: 'project_id' })
|
@Column({ name: 'project_id' })
|
||||||
projectId!: number;
|
projectId!: number;
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
|||||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
import { Audit } from '../../common/decorators/audit.decorator';
|
import { Audit } from '../../common/decorators/audit.decorator';
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
|
|
||||||
@ApiTags('Correspondences')
|
@ApiTags('Correspondences')
|
||||||
@Controller('correspondences')
|
@Controller('correspondences')
|
||||||
@@ -119,7 +120,7 @@ export class CorrespondenceController {
|
|||||||
return this.correspondenceService.findAll(searchDto);
|
return this.correspondenceService.findAll(searchDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/submit')
|
@Post(':uuid/submit')
|
||||||
@ApiOperation({ summary: 'Submit correspondence to Unified Workflow Engine' })
|
@ApiOperation({ summary: 'Submit correspondence to Unified Workflow Engine' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 201,
|
status: 201,
|
||||||
@@ -127,8 +128,8 @@ export class CorrespondenceController {
|
|||||||
})
|
})
|
||||||
@RequirePermission('correspondence.create')
|
@RequirePermission('correspondence.create')
|
||||||
@Audit('correspondence.submit', 'correspondence')
|
@Audit('correspondence.submit', 'correspondence')
|
||||||
submit(
|
async submit(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() submitDto: SubmitCorrespondenceDto,
|
@Body() submitDto: SubmitCorrespondenceDto,
|
||||||
@Request()
|
@Request()
|
||||||
req: Request & {
|
req: Request & {
|
||||||
@@ -138,28 +139,29 @@ export class CorrespondenceController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||||
// Extract roles from user assignments
|
// Extract roles from user assignments
|
||||||
const userRoles =
|
const userRoles =
|
||||||
req.user.assignments?.map((a) => a.role?.roleName).filter(Boolean) || [];
|
req.user.assignments?.map((a) => a.role?.roleName).filter(Boolean) || [];
|
||||||
|
|
||||||
// Use Unified Workflow Engine - pass user roles for DSL requirements check
|
// Use Unified Workflow Engine - pass user roles for DSL requirements check
|
||||||
return this.workflowService.submitWorkflow(
|
return this.workflowService.submitWorkflow(
|
||||||
id,
|
corr.id,
|
||||||
req.user.user_id,
|
req.user.user_id,
|
||||||
userRoles,
|
userRoles,
|
||||||
submitDto.note
|
submitDto.note
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get correspondence by ID' })
|
@ApiOperation({ summary: 'Get correspondence by UUID' })
|
||||||
@ApiResponse({ status: 200, description: 'Return correspondence details.' })
|
@ApiResponse({ status: 200, description: 'Return correspondence details.' })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.correspondenceService.findOne(id);
|
return this.correspondenceService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':uuid')
|
||||||
@ApiOperation({ summary: 'Update correspondence (Draft only)' })
|
@ApiOperation({ summary: 'Update correspondence (Draft only)' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -167,48 +169,52 @@ export class CorrespondenceController {
|
|||||||
})
|
})
|
||||||
@RequirePermission('correspondence.create') // Assuming create permission is enough for draft update, or add 'correspondence.edit'
|
@RequirePermission('correspondence.create') // Assuming create permission is enough for draft update, or add 'correspondence.edit'
|
||||||
@Audit('correspondence.update', 'correspondence')
|
@Audit('correspondence.update', 'correspondence')
|
||||||
update(
|
async update(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() updateDto: UpdateCorrespondenceDto,
|
@Body() updateDto: UpdateCorrespondenceDto,
|
||||||
@Request() req: Request & { user: unknown }
|
@Request() req: Request & { user: unknown }
|
||||||
) {
|
) {
|
||||||
|
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||||
return this.correspondenceService.update(
|
return this.correspondenceService.update(
|
||||||
id,
|
corr.id,
|
||||||
updateDto,
|
updateDto,
|
||||||
req.user as Parameters<typeof this.correspondenceService.create>[1]
|
req.user as Parameters<typeof this.correspondenceService.create>[1]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/references')
|
@Get(':uuid/references')
|
||||||
@ApiOperation({ summary: 'Get referenced documents' })
|
@ApiOperation({ summary: 'Get referenced documents' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Return list of referenced documents.',
|
description: 'Return list of referenced documents.',
|
||||||
})
|
})
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
getReferences(@Param('id', ParseIntPipe) id: number) {
|
async getReferences(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.correspondenceService.getReferences(id);
|
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||||
|
return this.correspondenceService.getReferences(corr.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/references')
|
@Post(':uuid/references')
|
||||||
@ApiOperation({ summary: 'Add reference to another document' })
|
@ApiOperation({ summary: 'Add reference to another document' })
|
||||||
@ApiResponse({ status: 201, description: 'Reference added successfully.' })
|
@ApiResponse({ status: 201, description: 'Reference added successfully.' })
|
||||||
@RequirePermission('document.edit')
|
@RequirePermission('document.edit')
|
||||||
addReference(
|
async addReference(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() dto: AddReferenceDto
|
@Body() dto: AddReferenceDto
|
||||||
) {
|
) {
|
||||||
return this.correspondenceService.addReference(id, dto);
|
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||||
|
return this.correspondenceService.addReference(corr.id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id/references/:targetId')
|
@Delete(':uuid/references/:targetId')
|
||||||
@ApiOperation({ summary: 'Remove reference' })
|
@ApiOperation({ summary: 'Remove reference' })
|
||||||
@ApiResponse({ status: 200, description: 'Reference removed successfully.' })
|
@ApiResponse({ status: 200, description: 'Reference removed successfully.' })
|
||||||
@RequirePermission('document.edit')
|
@RequirePermission('document.edit')
|
||||||
removeReference(
|
async removeReference(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Param('targetId', ParseIntPipe) targetId: number
|
@Param('targetId', ParseIntPipe) targetId: number
|
||||||
) {
|
) {
|
||||||
return this.correspondenceService.removeReference(id, targetId);
|
const corr = await this.correspondenceService.findOneByUuid(uuid);
|
||||||
|
return this.correspondenceService.removeReference(corr.id, targetId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -333,6 +333,26 @@ export class CorrespondenceService {
|
|||||||
return correspondence;
|
return correspondence;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOneByUuid(uuid: string) {
|
||||||
|
const correspondence = await this.correspondenceRepo.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
relations: [
|
||||||
|
'revisions',
|
||||||
|
'revisions.status',
|
||||||
|
'type',
|
||||||
|
'project',
|
||||||
|
'originator',
|
||||||
|
'recipients',
|
||||||
|
'recipients.recipientOrganization',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!correspondence) {
|
||||||
|
throw new NotFoundException(`Correspondence with UUID ${uuid} not found`);
|
||||||
|
}
|
||||||
|
return correspondence;
|
||||||
|
}
|
||||||
|
|
||||||
async addReference(id: number, dto: AddReferenceDto) {
|
async addReference(id: number, dto: AddReferenceDto) {
|
||||||
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
const source = await this.correspondenceRepo.findOne({ where: { id } });
|
||||||
const target = await this.correspondenceRepo.findOne({
|
const target = await this.correspondenceRepo.findOne({
|
||||||
|
|||||||
@@ -13,13 +13,16 @@ import { RfaRevision } from '../../rfa/entities/rfa-revision.entity';
|
|||||||
import { Correspondence } from './correspondence.entity';
|
import { Correspondence } from './correspondence.entity';
|
||||||
import { CorrespondenceStatus } from './correspondence-status.entity';
|
import { CorrespondenceStatus } from './correspondence-status.entity';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('correspondence_revisions')
|
@Entity('correspondence_revisions')
|
||||||
// ✅ เพิ่ม Index สำหรับ Virtual Columns เพื่อให้ Search เร็วขึ้น
|
// ✅ เพิ่ม Index สำหรับ Virtual Columns เพื่อให้ Search เร็วขึ้น
|
||||||
@Index('idx_corr_rev_v_project', ['vRefProjectId'])
|
@Index('idx_corr_rev_v_project', ['vRefProjectId'])
|
||||||
@Index('idx_corr_rev_v_type', ['vRefType'])
|
@Index('idx_corr_rev_v_type', ['vRefType'])
|
||||||
export class CorrespondenceRevision {
|
export class CorrespondenceRevision extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ name: 'correspondence_id' })
|
@Column({ name: 'correspondence_id' })
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ import { User } from '../../user/entities/user.entity';
|
|||||||
import { CorrespondenceRecipient } from './correspondence-recipient.entity';
|
import { CorrespondenceRecipient } from './correspondence-recipient.entity';
|
||||||
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
import { CorrespondenceRevision } from './correspondence-revision.entity';
|
||||||
import { Discipline } from '../../master/entities/discipline.entity';
|
import { Discipline } from '../../master/entities/discipline.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('correspondences')
|
@Entity('correspondences')
|
||||||
export class Correspondence {
|
export class Correspondence extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ name: 'correspondence_number', length: 100 })
|
@Column({ name: 'correspondence_number', length: 100 })
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
Query,
|
Query,
|
||||||
ParseIntPipe,
|
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -34,6 +33,7 @@ import { SearchAsBuiltDrawingDto } from './dto/search-asbuilt-drawing.dto';
|
|||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
import { Audit } from '../../common/decorators/audit.decorator';
|
import { Audit } from '../../common/decorators/audit.decorator';
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
|
|
||||||
@ApiTags('Drawings - AS Built')
|
@ApiTags('Drawings - AS Built')
|
||||||
@@ -56,16 +56,17 @@ export class AsBuiltDrawingController {
|
|||||||
return this.asBuiltDrawingService.create(createDto, user);
|
return this.asBuiltDrawingService.create(createDto, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/revisions')
|
@Post(':uuid/revisions')
|
||||||
@ApiOperation({ summary: 'Create new revision for AS Built Drawing' })
|
@ApiOperation({ summary: 'Create new revision for AS Built Drawing' })
|
||||||
@ApiResponse({ status: 201, description: 'Revision created' })
|
@ApiResponse({ status: 201, description: 'Revision created' })
|
||||||
@ApiResponse({ status: 404, description: 'AS Built Drawing not found' })
|
@ApiResponse({ status: 404, description: 'AS Built Drawing not found' })
|
||||||
@ApiResponse({ status: 409, description: 'Revision label already exists' })
|
@ApiResponse({ status: 409, description: 'Revision label already exists' })
|
||||||
async createRevision(
|
async createRevision(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() createDto: CreateAsBuiltDrawingRevisionDto
|
@Body() createDto: CreateAsBuiltDrawingRevisionDto
|
||||||
) {
|
) {
|
||||||
return this.asBuiltDrawingService.createRevision(id, createDto);
|
const drawing = await this.asBuiltDrawingService.findOneByUuid(uuid);
|
||||||
|
return this.asBuiltDrawingService.createRevision(drawing.id, createDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@@ -76,16 +77,16 @@ export class AsBuiltDrawingController {
|
|||||||
return this.asBuiltDrawingService.findAll(searchDto);
|
return this.asBuiltDrawingService.findAll(searchDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get AS Built Drawing by ID' })
|
@ApiOperation({ summary: 'Get AS Built Drawing by UUID' })
|
||||||
@ApiResponse({ status: 200, description: 'AS Built Drawing details' })
|
@ApiResponse({ status: 200, description: 'AS Built Drawing details' })
|
||||||
@ApiResponse({ status: 404, description: 'AS Built Drawing not found' })
|
@ApiResponse({ status: 404, description: 'AS Built Drawing not found' })
|
||||||
@RequirePermission('drawing.view')
|
@RequirePermission('drawing.view')
|
||||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
async findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.asBuiltDrawingService.findOne(id);
|
return this.asBuiltDrawingService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':uuid')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
@ApiOperation({ summary: 'Soft delete AS Built Drawing' })
|
@ApiOperation({ summary: 'Soft delete AS Built Drawing' })
|
||||||
@ApiResponse({ status: 204, description: 'AS Built Drawing deleted' })
|
@ApiResponse({ status: 204, description: 'AS Built Drawing deleted' })
|
||||||
@@ -93,9 +94,10 @@ export class AsBuiltDrawingController {
|
|||||||
@RequirePermission('drawing.delete')
|
@RequirePermission('drawing.delete')
|
||||||
@Audit('drawing.delete', 'asbuilt_drawing')
|
@Audit('drawing.delete', 'asbuilt_drawing')
|
||||||
async remove(
|
async remove(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@CurrentUser() user: User
|
@CurrentUser() user: User
|
||||||
) {
|
) {
|
||||||
return this.asBuiltDrawingService.remove(id, user);
|
const drawing = await this.asBuiltDrawingService.findOneByUuid(uuid);
|
||||||
|
return this.asBuiltDrawingService.remove(drawing.id, user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,6 +295,28 @@ export class AsBuiltDrawingService {
|
|||||||
return asBuiltDrawing;
|
return asBuiltDrawing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOneByUuid(uuid: string) {
|
||||||
|
const asBuiltDrawing = await this.asBuiltDrawingRepo.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
relations: [
|
||||||
|
'mainCategory',
|
||||||
|
'subCategory',
|
||||||
|
'revisions',
|
||||||
|
'revisions.attachments',
|
||||||
|
'revisions.shopDrawingRevisions',
|
||||||
|
],
|
||||||
|
order: {
|
||||||
|
revisions: { revisionNumber: 'DESC' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!asBuiltDrawing) {
|
||||||
|
throw new NotFoundException(`AS Built Drawing UUID ${uuid} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return asBuiltDrawing;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ลบ AS Built Drawing
|
* ลบ AS Built Drawing
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Put,
|
Put,
|
||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
ParseIntPipe,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
|
||||||
@@ -20,6 +19,7 @@ import { SearchContractDrawingDto } from './dto/search-contract-drawing.dto';
|
|||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
|
|
||||||
@@ -51,28 +51,33 @@ export class ContractDrawingController {
|
|||||||
return this.contractDrawingService.findAll(searchDto);
|
return this.contractDrawingService.findAll(searchDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get Contract Drawing details' })
|
@ApiOperation({ summary: 'Get Contract Drawing details' })
|
||||||
@RequirePermission('document.view')
|
@RequirePermission('document.view')
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.contractDrawingService.findOne(id);
|
return this.contractDrawingService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':uuid')
|
||||||
@ApiOperation({ summary: 'Update Contract Drawing' })
|
@ApiOperation({ summary: 'Update Contract Drawing' })
|
||||||
@RequirePermission('drawing.create') // สิทธิ์ ID 39 ครอบคลุมการแก้ไขด้วย
|
@RequirePermission('drawing.create') // สิทธิ์ ID 39 ครอบคลุมการแก้ไขด้วย
|
||||||
update(
|
async update(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() updateDto: UpdateContractDrawingDto,
|
@Body() updateDto: UpdateContractDrawingDto,
|
||||||
@CurrentUser() user: User
|
@CurrentUser() user: User
|
||||||
) {
|
) {
|
||||||
return this.contractDrawingService.update(id, updateDto, user);
|
const drawing = await this.contractDrawingService.findOneByUuid(uuid);
|
||||||
|
return this.contractDrawingService.update(drawing.id, updateDto, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':uuid')
|
||||||
@ApiOperation({ summary: 'Delete Contract Drawing (Soft Delete)' })
|
@ApiOperation({ summary: 'Delete Contract Drawing (Soft Delete)' })
|
||||||
@RequirePermission('document.delete') // สิทธิ์ ID 34: ลบเอกสาร
|
@RequirePermission('document.delete') // สิทธิ์ ID 34: ลบเอกสาร
|
||||||
remove(@Param('id', ParseIntPipe) id: number, @CurrentUser() user: User) {
|
async remove(
|
||||||
return this.contractDrawingService.remove(id, user);
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
|
@CurrentUser() user: User
|
||||||
|
) {
|
||||||
|
const drawing = await this.contractDrawingService.findOneByUuid(uuid);
|
||||||
|
return this.contractDrawingService.remove(drawing.id, user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,19 @@ export class ContractDrawingService {
|
|||||||
return drawing;
|
return drawing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOneByUuid(uuid: string) {
|
||||||
|
const drawing = await this.drawingRepo.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
relations: ['attachments'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!drawing) {
|
||||||
|
throw new NotFoundException(`Contract Drawing UUID ${uuid} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return drawing;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* แก้ไขข้อมูลแบบ (Update)
|
* แก้ไขข้อมูลแบบ (Update)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ import { AsBuiltDrawing } from './asbuilt-drawing.entity';
|
|||||||
import { ShopDrawingRevision } from './shop-drawing-revision.entity';
|
import { ShopDrawingRevision } from './shop-drawing-revision.entity';
|
||||||
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('asbuilt_drawing_revisions')
|
@Entity('asbuilt_drawing_revisions')
|
||||||
@Unique(['asBuiltDrawingId', 'isCurrent'])
|
@Unique(['asBuiltDrawingId', 'isCurrent'])
|
||||||
export class AsBuiltDrawingRevision {
|
export class AsBuiltDrawingRevision extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ name: 'asbuilt_drawing_id' })
|
@Column({ name: 'asbuilt_drawing_id' })
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ import { AsBuiltDrawingRevision } from './asbuilt-drawing-revision.entity';
|
|||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
|
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
|
||||||
import { ShopDrawingSubCategory } from './shop-drawing-sub-category.entity';
|
import { ShopDrawingSubCategory } from './shop-drawing-sub-category.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('asbuilt_drawings')
|
@Entity('asbuilt_drawings')
|
||||||
export class AsBuiltDrawing {
|
export class AsBuiltDrawing extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ name: 'project_id' })
|
@Column({ name: 'project_id' })
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ import { User } from '../../user/entities/user.entity';
|
|||||||
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
||||||
import { ContractDrawingSubcatCatMap } from './contract-drawing-subcat-cat-map.entity';
|
import { ContractDrawingSubcatCatMap } from './contract-drawing-subcat-cat-map.entity';
|
||||||
import { ContractDrawingVolume } from './contract-drawing-volume.entity';
|
import { ContractDrawingVolume } from './contract-drawing-volume.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('contract_drawings')
|
@Entity('contract_drawings')
|
||||||
export class ContractDrawing {
|
export class ContractDrawing extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number; // ! ห้ามว่าง
|
id!: number; // ! ห้ามว่าง
|
||||||
|
|
||||||
@Column({ name: 'project_id' })
|
@Column({ name: 'project_id' })
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ import { ShopDrawing } from './shop-drawing.entity';
|
|||||||
import { ContractDrawing } from './contract-drawing.entity';
|
import { ContractDrawing } from './contract-drawing.entity';
|
||||||
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
import { Attachment } from '../../../common/file-storage/entities/attachment.entity';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('shop_drawing_revisions')
|
@Entity('shop_drawing_revisions')
|
||||||
@Unique(['shopDrawingId', 'isCurrent'])
|
@Unique(['shopDrawingId', 'isCurrent'])
|
||||||
export class ShopDrawingRevision {
|
export class ShopDrawingRevision extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number; // เติม !
|
id!: number; // เติม !
|
||||||
|
|
||||||
@Column({ name: 'shop_drawing_id' })
|
@Column({ name: 'shop_drawing_id' })
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ import { ShopDrawingRevision } from './shop-drawing-revision.entity';
|
|||||||
import { Project } from '../../project/entities/project.entity';
|
import { Project } from '../../project/entities/project.entity';
|
||||||
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
|
import { ShopDrawingMainCategory } from './shop-drawing-main-category.entity';
|
||||||
import { ShopDrawingSubCategory } from './shop-drawing-sub-category.entity';
|
import { ShopDrawingSubCategory } from './shop-drawing-sub-category.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('shop_drawings')
|
@Entity('shop_drawings')
|
||||||
export class ShopDrawing {
|
export class ShopDrawing extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number; // เติม !
|
id!: number; // เติม !
|
||||||
|
|
||||||
@Column({ name: 'project_id' })
|
@Column({ name: 'project_id' })
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
ParseIntPipe,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
|
||||||
@@ -18,6 +17,7 @@ import { CreateShopDrawingRevisionDto } from './dto/create-shop-drawing-revision
|
|||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
import { Audit } from '../../common/decorators/audit.decorator'; // Import
|
||||||
@@ -44,21 +44,22 @@ export class ShopDrawingController {
|
|||||||
return this.shopDrawingService.findAll(searchDto);
|
return this.shopDrawingService.findAll(searchDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get Shop Drawing details with revisions' })
|
@ApiOperation({ summary: 'Get Shop Drawing details with revisions' })
|
||||||
@RequirePermission('drawing.view')
|
@RequirePermission('drawing.view')
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.shopDrawingService.findOne(id);
|
return this.shopDrawingService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/revisions')
|
@Post(':uuid/revisions')
|
||||||
@ApiOperation({ summary: 'Add new revision to existing Shop Drawing' })
|
@ApiOperation({ summary: 'Add new revision to existing Shop Drawing' })
|
||||||
@RequirePermission('drawing.create') // หรือ drawing.edit ตาม Logic องค์กร
|
@RequirePermission('drawing.create') // หรือ drawing.edit ตาม Logic องค์กร
|
||||||
@Audit('drawing.create', 'shop_drawing') // ✅ แปะตรงนี้
|
@Audit('drawing.create', 'shop_drawing') // ✅ แปะตรงนี้
|
||||||
createRevision(
|
async createRevision(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() createRevisionDto: CreateShopDrawingRevisionDto,
|
@Body() createRevisionDto: CreateShopDrawingRevisionDto
|
||||||
) {
|
) {
|
||||||
return this.shopDrawingService.createRevision(id, createRevisionDto);
|
const sd = await this.shopDrawingService.findOneByUuid(uuid);
|
||||||
|
return this.shopDrawingService.createRevision(sd.id, createRevisionDto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,6 +289,28 @@ export class ShopDrawingService {
|
|||||||
return shopDrawing;
|
return shopDrawing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOneByUuid(uuid: string) {
|
||||||
|
const shopDrawing = await this.shopDrawingRepo.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
relations: [
|
||||||
|
'mainCategory',
|
||||||
|
'subCategory',
|
||||||
|
'revisions',
|
||||||
|
'revisions.attachments',
|
||||||
|
'revisions.contractDrawings',
|
||||||
|
],
|
||||||
|
order: {
|
||||||
|
revisions: { revisionNumber: 'DESC' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shopDrawing) {
|
||||||
|
throw new NotFoundException(`Shop Drawing UUID ${uuid} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shopDrawing;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ลบ Shop Drawing
|
* ลบ Shop Drawing
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
PrimaryColumn, // ✅ [Fix] เพิ่ม Import นี้
|
PrimaryColumn, // ✅ [Fix] เพิ่ม Import นี้
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from '../../user/entities/user.entity';
|
import { User } from '../../user/entities/user.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
export enum NotificationType {
|
export enum NotificationType {
|
||||||
EMAIL = 'EMAIL',
|
EMAIL = 'EMAIL',
|
||||||
@@ -16,8 +18,9 @@ export enum NotificationType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Entity('notifications')
|
@Entity('notifications')
|
||||||
export class Notification {
|
export class Notification extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ name: 'user_id' })
|
@Column({ name: 'user_id' })
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Controller, Get, Put, Param, UseGuards, Query } from '@nestjs/common';
|
||||||
Controller,
|
|
||||||
Get,
|
|
||||||
Put,
|
|
||||||
Param,
|
|
||||||
UseGuards,
|
|
||||||
ParseIntPipe,
|
|
||||||
Query,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
@@ -17,6 +9,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
|||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
import { User } from '../user/entities/user.entity';
|
import { User } from '../user/entities/user.entity';
|
||||||
import { SearchNotificationDto } from './dto/search-notification.dto'; // ✅ Import
|
import { SearchNotificationDto } from './dto/search-notification.dto'; // ✅ Import
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
|
|
||||||
@ApiTags('Notifications')
|
@ApiTags('Notifications')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -26,14 +19,14 @@ export class NotificationController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly notificationService: NotificationService,
|
private readonly notificationService: NotificationService,
|
||||||
@InjectRepository(Notification)
|
@InjectRepository(Notification)
|
||||||
private notificationRepo: Repository<Notification>,
|
private notificationRepo: Repository<Notification>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Get my notifications' })
|
@ApiOperation({ summary: 'Get my notifications' })
|
||||||
async getMyNotifications(
|
async getMyNotifications(
|
||||||
@CurrentUser() user: User,
|
@CurrentUser() user: User,
|
||||||
@Query() searchDto: SearchNotificationDto, // ✅ ใช้ DTO แทน
|
@Query() searchDto: SearchNotificationDto // ✅ ใช้ DTO แทน
|
||||||
) {
|
) {
|
||||||
const { page = 1, limit = 20, isRead } = searchDto;
|
const { page = 1, limit = 20, isRead } = searchDto;
|
||||||
|
|
||||||
@@ -65,13 +58,13 @@ export class NotificationController {
|
|||||||
return { unreadCount: count };
|
return { unreadCount: count };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id/read')
|
@Put(':uuid/read')
|
||||||
@ApiOperation({ summary: 'Mark notification as read' })
|
@ApiOperation({ summary: 'Mark notification as read' })
|
||||||
async markAsRead(
|
async markAsRead(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@CurrentUser() user: User,
|
@CurrentUser() user: User
|
||||||
) {
|
) {
|
||||||
return this.notificationService.markAsRead(id, user.user_id);
|
return this.notificationService.markAsReadByUuid(uuid, user.user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('read-all')
|
@Put('read-all')
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export class NotificationService {
|
|||||||
@InjectRepository(User)
|
@InjectRepository(User)
|
||||||
private userRepo: Repository<User>,
|
private userRepo: Repository<User>,
|
||||||
// ไม่ต้อง Inject UserPrefRepo แล้ว เพราะ Processor จะจัดการเอง
|
// ไม่ต้อง Inject UserPrefRepo แล้ว เพราะ Processor จะจัดการเอง
|
||||||
private notificationGateway: NotificationGateway,
|
private notificationGateway: NotificationGateway
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,14 +84,14 @@ export class NotificationService {
|
|||||||
delay: 5000,
|
delay: 5000,
|
||||||
},
|
},
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.debug(`Dispatched notification job for user ${data.userId}`);
|
this.logger.debug(`Dispatched notification job for user ${data.userId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to process notification for user ${data.userId}`,
|
`Failed to process notification for user ${data.userId}`,
|
||||||
(error as Error).stack,
|
(error as Error).stack
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,10 +154,25 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markAsReadByUuid(uuid: string, userId: number): Promise<void> {
|
||||||
|
const notification = await this.notificationRepo.findOne({
|
||||||
|
where: { uuid, userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!notification) {
|
||||||
|
throw new NotFoundException(`Notification UUID ${uuid} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notification.isRead) {
|
||||||
|
notification.isRead = true;
|
||||||
|
await this.notificationRepo.save(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async markAllAsRead(userId: number): Promise<void> {
|
async markAllAsRead(userId: number): Promise<void> {
|
||||||
await this.notificationRepo.update(
|
await this.notificationRepo.update(
|
||||||
{ userId, isRead: false },
|
{ userId, isRead: false },
|
||||||
{ isRead: true },
|
{ isRead: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ import {
|
|||||||
JoinColumn,
|
JoinColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { OrganizationRole } from './organization-role.entity';
|
import { OrganizationRole } from './organization-role.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('organizations')
|
@Entity('organizations')
|
||||||
export class Organization {
|
export class Organization extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
@Column({ name: 'organization_code', length: 20, unique: true })
|
@Column({ name: 'organization_code', length: 20, unique: true })
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
ParseIntPipe,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
import { OrganizationService } from './organization.service.js';
|
import { OrganizationService } from './organization.service.js';
|
||||||
@@ -17,6 +16,7 @@ import { UpdateOrganizationDto } from './dto/update-organization.dto.js';
|
|||||||
import { SearchOrganizationDto } from './dto/search-organization.dto.js';
|
import { SearchOrganizationDto } from './dto/search-organization.dto.js';
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard.js';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator.js';
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
|
|
||||||
@ApiTags('Organizations')
|
@ApiTags('Organizations')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -38,26 +38,26 @@ export class OrganizationController {
|
|||||||
return this.orgService.findAll(query);
|
return this.orgService.findAll(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get Organization by ID' })
|
@ApiOperation({ summary: 'Get Organization by UUID' })
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.orgService.findOne(id);
|
return this.orgService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':uuid')
|
||||||
@RequirePermission('master_data.manage')
|
@RequirePermission('master_data.manage')
|
||||||
@ApiOperation({ summary: 'Update Organization' })
|
@ApiOperation({ summary: 'Update Organization' })
|
||||||
update(
|
update(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() dto: UpdateOrganizationDto
|
@Body() dto: UpdateOrganizationDto
|
||||||
) {
|
) {
|
||||||
return this.orgService.update(id, dto);
|
return this.orgService.update(uuid, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':uuid')
|
||||||
@RequirePermission('master_data.manage')
|
@RequirePermission('master_data.manage')
|
||||||
@ApiOperation({ summary: 'Delete Organization' })
|
@ApiOperation({ summary: 'Delete Organization' })
|
||||||
remove(@Param('id', ParseIntPipe) id: number) {
|
remove(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.orgService.remove(id);
|
return this.orgService.remove(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,17 +86,21 @@ export class OrganizationService {
|
|||||||
return org;
|
return org;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: number, dto: UpdateOrganizationDto) {
|
async findOneByUuid(uuid: string) {
|
||||||
const org = await this.findOne(id);
|
const org = await this.orgRepo.findOne({ where: { uuid } });
|
||||||
|
if (!org)
|
||||||
|
throw new NotFoundException(`Organization UUID ${uuid} not found`);
|
||||||
|
return org;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(uuid: string, dto: UpdateOrganizationDto) {
|
||||||
|
const org = await this.findOneByUuid(uuid);
|
||||||
Object.assign(org, dto);
|
Object.assign(org, dto);
|
||||||
return this.orgRepo.save(org);
|
return this.orgRepo.save(org);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: number) {
|
async remove(uuid: string) {
|
||||||
const org = await this.findOne(id);
|
const org = await this.findOneByUuid(uuid);
|
||||||
// Hard delete or Soft delete? Schema doesn't have deleted_at for Organization, but let's check.
|
|
||||||
// Schema says: created_at, updated_at. No deleted_at.
|
|
||||||
// So hard delete.
|
|
||||||
return this.orgRepo.remove(org);
|
return this.orgRepo.remove(org);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,36 @@
|
|||||||
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
import {
|
||||||
|
Entity,
|
||||||
|
Column,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
OneToMany,
|
||||||
|
BeforeInsert,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
import { Contract } from '../../contract/entities/contract.entity';
|
import { Contract } from '../../contract/entities/contract.entity';
|
||||||
|
|
||||||
@Entity('projects')
|
@Entity('projects')
|
||||||
export class Project extends BaseEntity {
|
export class Project extends BaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
@Exclude()
|
||||||
id!: number;
|
id!: number;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'uuid',
|
||||||
|
unique: true,
|
||||||
|
nullable: false,
|
||||||
|
comment: 'UUID Public Identifier (ADR-019)',
|
||||||
|
})
|
||||||
|
uuid!: string;
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
generateUuid(): void {
|
||||||
|
if (!this.uuid) {
|
||||||
|
this.uuid = uuidv7();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Column({ name: 'project_code', unique: true, length: 50 })
|
@Column({ name: 'project_code', unique: true, length: 50 })
|
||||||
projectCode!: string;
|
projectCode!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
ParseIntPipe,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
|
||||||
@@ -19,6 +18,7 @@ import { SearchProjectDto } from './dto/search-project.dto';
|
|||||||
|
|
||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
|
|
||||||
@ApiTags('Projects')
|
@ApiTags('Projects')
|
||||||
@@ -49,34 +49,34 @@ export class ProjectController {
|
|||||||
return this.projectService.findAllOrganizations();
|
return this.projectService.findAllOrganizations();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/contracts')
|
@Get(':uuid/contracts')
|
||||||
@ApiOperation({ summary: 'List All Contracts in Project' })
|
@ApiOperation({ summary: 'List All Contracts in Project' })
|
||||||
@RequirePermission('project.view')
|
@RequirePermission('project.view')
|
||||||
findContracts(@Param('id', ParseIntPipe) id: number) {
|
findContracts(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.projectService.findContracts(id);
|
return this.projectService.findContracts(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get Project Details' })
|
@ApiOperation({ summary: 'Get Project Details' })
|
||||||
@RequirePermission('project.view')
|
@RequirePermission('project.view')
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.projectService.findOne(id);
|
return this.projectService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':uuid')
|
||||||
@ApiOperation({ summary: 'Update Project' })
|
@ApiOperation({ summary: 'Update Project' })
|
||||||
@RequirePermission('project.edit')
|
@RequirePermission('project.edit')
|
||||||
update(
|
update(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() updateDto: UpdateProjectDto
|
@Body() updateDto: UpdateProjectDto
|
||||||
) {
|
) {
|
||||||
return this.projectService.update(id, updateDto);
|
return this.projectService.update(uuid, updateDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':uuid')
|
||||||
@ApiOperation({ summary: 'Delete Project (Soft Delete)' })
|
@ApiOperation({ summary: 'Delete Project (Soft Delete)' })
|
||||||
@RequirePermission('project.delete')
|
@RequirePermission('project.delete')
|
||||||
remove(@Param('id', ParseIntPipe) id: number) {
|
remove(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.projectService.remove(id);
|
return this.projectService.remove(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,8 +91,21 @@ export class ProjectService {
|
|||||||
return project;
|
return project;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: number, updateDto: UpdateProjectDto) {
|
async findOneByUuid(uuid: string) {
|
||||||
const project = await this.findOne(id);
|
const project = await this.projectRepository.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
relations: ['contracts'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
throw new NotFoundException(`Project UUID ${uuid} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(uuid: string, updateDto: UpdateProjectDto) {
|
||||||
|
const project = await this.findOneByUuid(uuid);
|
||||||
|
|
||||||
// Merge ข้อมูลใหม่ใส่ข้อมูลเดิม
|
// Merge ข้อมูลใหม่ใส่ข้อมูลเดิม
|
||||||
this.projectRepository.merge(project, updateDto);
|
this.projectRepository.merge(project, updateDto);
|
||||||
@@ -100,22 +113,14 @@ export class ProjectService {
|
|||||||
return this.projectRepository.save(project);
|
return this.projectRepository.save(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(id: number) {
|
async remove(uuid: string) {
|
||||||
const project = await this.findOne(id);
|
const project = await this.findOneByUuid(uuid);
|
||||||
// ใช้ Soft Delete
|
// ใช้ Soft Delete
|
||||||
return this.projectRepository.softRemove(project);
|
return this.projectRepository.softRemove(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findContracts(projectId: number) {
|
async findContracts(uuid: string) {
|
||||||
const project = await this.projectRepository.findOne({
|
const project = await this.findOneByUuid(uuid);
|
||||||
where: { id: projectId },
|
|
||||||
relations: ['contracts'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
|
||||||
throw new NotFoundException(`Project ID ${projectId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return project.contracts;
|
return project.contracts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ import {
|
|||||||
import { Organization } from '../../organization/entities/organization.entity'; // Adjust path as needed
|
import { Organization } from '../../organization/entities/organization.entity'; // Adjust path as needed
|
||||||
import { UserAssignment } from './user-assignment.entity';
|
import { UserAssignment } from './user-assignment.entity';
|
||||||
import { UserPreference } from './user-preference.entity';
|
import { UserPreference } from './user-preference.entity';
|
||||||
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('users')
|
@Entity('users')
|
||||||
export class User {
|
export class User extends UuidBaseEntity {
|
||||||
@PrimaryGeneratedColumn({ name: 'user_id' })
|
@PrimaryGeneratedColumn({ name: 'user_id' })
|
||||||
|
@Exclude()
|
||||||
user_id!: number;
|
user_id!: number;
|
||||||
|
|
||||||
@Column({ unique: true, length: 50 })
|
@Column({ unique: true, length: 50 })
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
|||||||
import { RbacGuard } from '../../common/guards/rbac.guard';
|
import { RbacGuard } from '../../common/guards/rbac.guard';
|
||||||
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
|
import { ParseUuidPipe } from '../../common/pipes/parse-uuid.pipe';
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
|
|
||||||
@ApiTags('Users')
|
@ApiTags('Users')
|
||||||
@@ -123,35 +124,35 @@ export class UserController {
|
|||||||
return this.userService.findAll(query);
|
return this.userService.findAll(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':uuid')
|
||||||
@ApiOperation({ summary: 'Get user details' })
|
@ApiOperation({ summary: 'Get user details' })
|
||||||
@ApiParam({ name: 'id', description: 'User ID' })
|
@ApiParam({ name: 'uuid', description: 'User UUID' })
|
||||||
@ApiResponse({ status: 200, description: 'User details' })
|
@ApiResponse({ status: 200, description: 'User details' })
|
||||||
@RequirePermission('user.view')
|
@RequirePermission('user.view')
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.userService.findOne(id);
|
return this.userService.findOneByUuid(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':uuid')
|
||||||
@ApiOperation({ summary: 'Update user' })
|
@ApiOperation({ summary: 'Update user' })
|
||||||
@ApiParam({ name: 'id', description: 'User ID' })
|
@ApiParam({ name: 'uuid', description: 'User UUID' })
|
||||||
@ApiBody({ type: UpdateUserDto })
|
@ApiBody({ type: UpdateUserDto })
|
||||||
@ApiResponse({ status: 200, description: 'User updated' })
|
@ApiResponse({ status: 200, description: 'User updated' })
|
||||||
@RequirePermission('user.edit')
|
@RequirePermission('user.edit')
|
||||||
update(
|
update(
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('uuid', ParseUuidPipe) uuid: string,
|
||||||
@Body() updateUserDto: UpdateUserDto
|
@Body() updateUserDto: UpdateUserDto
|
||||||
) {
|
) {
|
||||||
return this.userService.update(id, updateUserDto);
|
return this.userService.update(uuid, updateUserDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':uuid')
|
||||||
@ApiOperation({ summary: 'Delete user (Soft delete)' })
|
@ApiOperation({ summary: 'Delete user (Soft delete)' })
|
||||||
@ApiParam({ name: 'id', description: 'User ID' })
|
@ApiParam({ name: 'uuid', description: 'User UUID' })
|
||||||
@ApiResponse({ status: 200, description: 'User deleted' })
|
@ApiResponse({ status: 200, description: 'User deleted' })
|
||||||
@RequirePermission('user.delete')
|
@RequirePermission('user.delete')
|
||||||
remove(@Param('id', ParseIntPipe) id: number) {
|
remove(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
return this.userService.remove(id);
|
return this.userService.remove(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Role Assignment ---
|
// --- Role Assignment ---
|
||||||
|
|||||||
@@ -133,13 +133,31 @@ export class UserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOneByUuid(uuid: string): Promise<User> {
|
||||||
|
const user = await this.usersRepository.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
relations: [
|
||||||
|
'preference',
|
||||||
|
'assignments',
|
||||||
|
'assignments.role',
|
||||||
|
'assignments.role.permissions',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(`User with UUID ${uuid} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
async findOneByUsername(username: string): Promise<User | null> {
|
async findOneByUsername(username: string): Promise<User | null> {
|
||||||
return this.usersRepository.findOne({ where: { username } });
|
return this.usersRepository.findOne({ where: { username } });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. แก้ไขข้อมูล
|
// 4. แก้ไขข้อมูล
|
||||||
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
|
async update(uuid: string, updateUserDto: UpdateUserDto): Promise<User> {
|
||||||
const user = await this.findOne(id);
|
const user = await this.findOneByUuid(uuid);
|
||||||
|
|
||||||
if (updateUserDto.password) {
|
if (updateUserDto.password) {
|
||||||
const salt = await bcrypt.genSalt();
|
const salt = await bcrypt.genSalt();
|
||||||
@@ -150,20 +168,21 @@ export class UserService {
|
|||||||
const savedUser = await this.usersRepository.save(updatedUser);
|
const savedUser = await this.usersRepository.save(updatedUser);
|
||||||
|
|
||||||
// ⚠️ สำคัญ: เมื่อมีการแก้ไขข้อมูล User ต้องเคลียร์ Cache สิทธิ์เสมอ
|
// ⚠️ สำคัญ: เมื่อมีการแก้ไขข้อมูล User ต้องเคลียร์ Cache สิทธิ์เสมอ
|
||||||
await this.clearUserCache(id);
|
await this.clearUserCache(user.user_id);
|
||||||
|
|
||||||
return savedUser;
|
return savedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. ลบผู้ใช้ (Soft Delete)
|
// 5. ลบผู้ใช้ (Soft Delete)
|
||||||
async remove(id: number): Promise<void> {
|
async remove(uuid: string): Promise<void> {
|
||||||
const result = await this.usersRepository.softDelete(id);
|
const user = await this.findOneByUuid(uuid);
|
||||||
|
const result = await this.usersRepository.softDelete(user.user_id);
|
||||||
|
|
||||||
if (result.affected === 0) {
|
if (result.affected === 0) {
|
||||||
throw new NotFoundException(`User with ID ${id} not found`);
|
throw new NotFoundException(`User with UUID ${uuid} not found`);
|
||||||
}
|
}
|
||||||
// เคลียร์ Cache เมื่อลบ
|
// เคลียร์ Cache เมื่อลบ
|
||||||
await this.clearUserCache(id);
|
await this.clearUserCache(user.user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findDocControlIdByOrg(organizationId: number): Promise<number | null> {
|
async findDocControlIdByOrg(organizationId: number): Promise<number | null> {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default function OrganizationsPage() {
|
|||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
if (orgToDelete) {
|
if (orgToDelete) {
|
||||||
deleteOrg.mutate(orgToDelete.id, {
|
deleteOrg.mutate(orgToDelete.uuid, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
setOrgToDelete(null);
|
setOrgToDelete(null);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default function UsersPage() {
|
|||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
if (userToDelete) {
|
if (userToDelete) {
|
||||||
deleteMutation.mutate(userToDelete.userId, {
|
deleteMutation.mutate(userToDelete.uuid, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
setUserToDelete(null);
|
setUserToDelete(null);
|
||||||
@@ -186,7 +186,7 @@ export default function UsersPage() {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Organizations</SelectItem>
|
<SelectItem value="all">All Organizations</SelectItem>
|
||||||
{Array.isArray(organizations) && (organizations as Organization[]).map((org) => (
|
{Array.isArray(organizations) && (organizations as Organization[]).map((org) => (
|
||||||
<SelectItem key={org.id} value={org.id.toString()}>
|
<SelectItem key={org.uuid} value={(org.id ?? org.uuid).toString()}>
|
||||||
{org.organizationCode} - {org.organizationName}
|
{org.organizationCode} - {org.organizationName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ interface Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Contract {
|
interface Contract {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
contractCode: string;
|
contractCode: string;
|
||||||
contractName: string;
|
contractName: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
@@ -112,7 +113,7 @@ export default function ContractsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updateContract = useMutation({
|
const updateContract = useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number, data: UpdateContractDto }) => apiClient.patch(`/contracts/${id}`, data).then(res => res.data),
|
mutationFn: ({ uuid, data }: { uuid: string, data: UpdateContractDto }) => apiClient.patch(`/contracts/${uuid}`, data).then(res => res.data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Contract updated successfully");
|
toast.success("Contract updated successfully");
|
||||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||||
@@ -122,7 +123,7 @@ export default function ContractsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const deleteContract = useMutation({
|
const deleteContract = useMutation({
|
||||||
mutationFn: (id: number) => apiClient.delete(`/contracts/${id}`).then(res => res.data),
|
mutationFn: (uuid: string) => apiClient.delete(`/contracts/${uuid}`).then(res => res.data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Contract deleted successfully");
|
toast.success("Contract deleted successfully");
|
||||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||||
@@ -131,7 +132,7 @@ export default function ContractsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [editingId, setEditingId] = useState<number | null>(null);
|
const [editingUuid, setEditingUuid] = useState<string | null>(null);
|
||||||
|
|
||||||
// Stats for Delete Dialog
|
// Stats for Delete Dialog
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
@@ -144,7 +145,7 @@ export default function ContractsPage() {
|
|||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
if (contractToDelete) {
|
if (contractToDelete) {
|
||||||
deleteContract.mutate(contractToDelete.id, {
|
deleteContract.mutate(contractToDelete.uuid, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
setContractToDelete(null);
|
setContractToDelete(null);
|
||||||
@@ -212,7 +213,7 @@ export default function ContractsPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleEdit = (contract: Contract) => {
|
const handleEdit = (contract: Contract) => {
|
||||||
setEditingId(contract.id);
|
setEditingUuid(contract.uuid);
|
||||||
reset({
|
reset({
|
||||||
contractCode: contract.contractCode,
|
contractCode: contract.contractCode,
|
||||||
contractName: contract.contractName,
|
contractName: contract.contractName,
|
||||||
@@ -225,7 +226,7 @@ export default function ContractsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
setEditingId(null);
|
setEditingUuid(null);
|
||||||
reset({
|
reset({
|
||||||
contractCode: "",
|
contractCode: "",
|
||||||
contractName: "",
|
contractName: "",
|
||||||
@@ -243,8 +244,8 @@ export default function ContractsPage() {
|
|||||||
projectId: parseInt(data.projectId),
|
projectId: parseInt(data.projectId),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editingId) {
|
if (editingUuid) {
|
||||||
updateContract.mutate({ id: editingId, data: submitData });
|
updateContract.mutate({ uuid: editingUuid, data: submitData });
|
||||||
} else {
|
} else {
|
||||||
createContract.mutate(submitData);
|
createContract.mutate(submitData);
|
||||||
}
|
}
|
||||||
@@ -289,7 +290,7 @@ export default function ContractsPage() {
|
|||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{editingId ? "Edit Contract" : "New Contract"}</DialogTitle>
|
<DialogTitle>{editingUuid ? "Edit Contract" : "New Contract"}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
|
||||||
@@ -363,7 +364,7 @@ export default function ContractsPage() {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={createContract.isPending || updateContract.isPending}>
|
<Button type="submit" disabled={createContract.isPending || updateContract.isPending}>
|
||||||
{editingId ? "Save Changes" : "Create Contract"}
|
{editingUuid ? "Save Changes" : "Create Contract"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ import {
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
projectCode: string;
|
projectCode: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -69,7 +70,7 @@ export default function ProjectsPage() {
|
|||||||
const deleteProject = useDeleteProject();
|
const deleteProject = useDeleteProject();
|
||||||
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [editingId, setEditingId] = useState<number | null>(null);
|
const [editingUuid, setEditingUuid] = useState<string | null>(null);
|
||||||
|
|
||||||
// Stats for Delete Dialog
|
// Stats for Delete Dialog
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
@@ -82,7 +83,7 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
if (projectToDelete) {
|
if (projectToDelete) {
|
||||||
deleteProject.mutate(projectToDelete.id, {
|
deleteProject.mutate(projectToDelete.uuid, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
setProjectToDelete(null);
|
setProjectToDelete(null);
|
||||||
@@ -156,7 +157,7 @@ export default function ProjectsPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleEdit = (project: Project) => {
|
const handleEdit = (project: Project) => {
|
||||||
setEditingId(project.id);
|
setEditingUuid(project.uuid);
|
||||||
reset({
|
reset({
|
||||||
projectCode: project.projectCode,
|
projectCode: project.projectCode,
|
||||||
projectName: project.projectName,
|
projectName: project.projectName,
|
||||||
@@ -166,7 +167,7 @@ export default function ProjectsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
setEditingId(null);
|
setEditingUuid(null);
|
||||||
reset({
|
reset({
|
||||||
projectCode: "",
|
projectCode: "",
|
||||||
projectName: "",
|
projectName: "",
|
||||||
@@ -176,9 +177,9 @@ export default function ProjectsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (data: ProjectFormData) => {
|
const onSubmit = (data: ProjectFormData) => {
|
||||||
if (editingId) {
|
if (editingUuid) {
|
||||||
updateProject.mutate(
|
updateProject.mutate(
|
||||||
{ id: editingId, data },
|
{ uuid: editingUuid, data },
|
||||||
{
|
{
|
||||||
onSuccess: () => setDialogOpen(false),
|
onSuccess: () => setDialogOpen(false),
|
||||||
}
|
}
|
||||||
@@ -232,7 +233,7 @@ export default function ProjectsPage() {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{editingId ? "Edit Project" : "New Project"}
|
{editingUuid ? "Edit Project" : "New Project"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
@@ -241,7 +242,7 @@ export default function ProjectsPage() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="e.g. LCBP3"
|
placeholder="e.g. LCBP3"
|
||||||
{...register("projectCode")}
|
{...register("projectCode")}
|
||||||
disabled={!!editingId} // Code is immutable after creation usually
|
disabled={!!editingUuid} // Code is immutable after creation usually
|
||||||
/>
|
/>
|
||||||
{errors.projectCode && (
|
{errors.projectCode && (
|
||||||
<p className="text-sm text-red-500">{errors.projectCode.message}</p>
|
<p className="text-sm text-red-500">{errors.projectCode.message}</p>
|
||||||
@@ -280,7 +281,7 @@ export default function ProjectsPage() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
disabled={createProject.isPending || updateProject.isPending}
|
disabled={createProject.isPending || updateProject.isPending}
|
||||||
>
|
>
|
||||||
{editingId ? "Save Changes" : "Create Project"}
|
{editingUuid ? "Save Changes" : "Create Project"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+7
-8
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { circulationService } from "@/lib/services/circulation.service";
|
import { circulationService } from "@/lib/services/circulation.service";
|
||||||
import { Circulation, UpdateCirculationRoutingDto } from "@/types/circulation";
|
import { Circulation, UpdateCirculationRoutingDto } from "@/types/circulation";
|
||||||
@@ -42,14 +42,13 @@ function getStatusVariant(status: string): "default" | "secondary" | "destructiv
|
|||||||
|
|
||||||
export default function CirculationDetailPage() {
|
export default function CirculationDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const id = params.id as string;
|
const uuid = params.uuid as string;
|
||||||
|
|
||||||
const { data: circulation, isLoading, error } = useQuery<Circulation>({
|
const { data: circulation, isLoading, error } = useQuery<Circulation>({
|
||||||
queryKey: ["circulation", id],
|
queryKey: ["circulation", uuid],
|
||||||
queryFn: () => circulationService.getById(id),
|
queryFn: () => circulationService.getByUuid(uuid),
|
||||||
enabled: !!id,
|
enabled: !!uuid,
|
||||||
});
|
});
|
||||||
|
|
||||||
const completeMutation = useMutation({
|
const completeMutation = useMutation({
|
||||||
@@ -57,7 +56,7 @@ export default function CirculationDetailPage() {
|
|||||||
circulationService.updateRouting(routingId, data),
|
circulationService.updateRouting(routingId, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Task completed successfully");
|
toast.success("Task completed successfully");
|
||||||
queryClient.invalidateQueries({ queryKey: ["circulation", id] });
|
queryClient.invalidateQueries({ queryKey: ["circulation", uuid] });
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error("Failed to update task status");
|
toast.error("Failed to update task status");
|
||||||
@@ -146,7 +145,7 @@ export default function CirculationDetailPage() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Linked Document</p>
|
<p className="text-sm text-muted-foreground">Linked Document</p>
|
||||||
<Link
|
<Link
|
||||||
href={`/correspondences/${circulation.correspondenceId}`}
|
href={`/correspondences/${circulation.correspondence.uuid}`}
|
||||||
className="font-medium text-primary hover:underline"
|
className="font-medium text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{circulation.correspondence.correspondence_number}
|
{circulation.correspondence.correspondence_number}
|
||||||
@@ -83,7 +83,7 @@ export default function CreateCirculationPage() {
|
|||||||
mutationFn: (data: CreateCirculationDto) => circulationService.create(data),
|
mutationFn: (data: CreateCirculationDto) => circulationService.create(data),
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
toast.success("Circulation created successfully");
|
toast.success("Circulation created successfully");
|
||||||
router.push(`/circulation/${result.id}`);
|
router.push(`/circulation/${result.uuid}`);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error("Failed to create circulation");
|
toast.error("Failed to create circulation");
|
||||||
@@ -232,7 +232,7 @@ export default function CreateCirculationPage() {
|
|||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{selectedAssignees.map((userId) => {
|
{selectedAssignees.map((userId) => {
|
||||||
const user = users.find(
|
const user = users.find(
|
||||||
(u: { userId: number }) => u.userId === userId
|
(u) => u.userId === userId
|
||||||
);
|
);
|
||||||
return user ? (
|
return user ? (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -267,16 +267,16 @@ export default function CreateCirculationPage() {
|
|||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No user found.</CommandEmpty>
|
<CommandEmpty>No user found.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{users.map((user: { userId: number; username: string; firstName?: string; lastName?: string }) => (
|
{users.map((user) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={user.userId}
|
key={user.userId ?? user.uuid}
|
||||||
value={user.username}
|
value={user.username}
|
||||||
onSelect={() => toggleAssignee(user.userId)}
|
onSelect={() => user.userId && toggleAssignee(user.userId)}
|
||||||
>
|
>
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-4 w-4",
|
"mr-2 h-4 w-4",
|
||||||
selectedAssignees.includes(user.userId)
|
user.userId != null && selectedAssignees.includes(user.userId)
|
||||||
? "opacity-100"
|
? "opacity-100"
|
||||||
: "opacity-0"
|
: "opacity-0"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { CorrespondenceForm } from "@/components/correspondences/form";
|
|
||||||
import { useCorrespondence } from "@/hooks/use-correspondence";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
|
|
||||||
export default function EditCorrespondencePage() {
|
|
||||||
const params = useParams();
|
|
||||||
const id = Number(params?.id);
|
|
||||||
|
|
||||||
const { data: correspondence, isLoading, isError } = useCorrespondence(id);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex bg-muted/20 min-h-screen justify-center items-center">
|
|
||||||
<Loader2 className="h-8 w-8 animate-spin" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError || !correspondence) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
|
||||||
<h1 className="text-xl font-bold text-red-500">Failed to load correspondence</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-4xl mx-auto py-6">
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="text-3xl font-bold">Edit Correspondence</h1>
|
|
||||||
<p className="text-muted-foreground mt-1">
|
|
||||||
{correspondence.correspondenceNumber}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-card border rounded-lg p-6 shadow-sm">
|
|
||||||
<CorrespondenceForm initialData={correspondence} id={id} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+7
-13
@@ -3,27 +3,22 @@
|
|||||||
import { CorrespondenceDetail } from "@/components/correspondences/detail";
|
import { CorrespondenceDetail } from "@/components/correspondences/detail";
|
||||||
import { useCorrespondence } from "@/hooks/use-correspondence";
|
import { useCorrespondence } from "@/hooks/use-correspondence";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { notFound, useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
|
|
||||||
export default function CorrespondenceDetailPage() {
|
export default function CorrespondenceDetailPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = Number(params?.id); // useParams returns string | string[]
|
const uuid = (params?.uuid as string) ?? '';
|
||||||
|
|
||||||
if (isNaN(id)) {
|
const { data: correspondence, isLoading, isError } = useCorrespondence(uuid);
|
||||||
// We can't use notFound() directly in client component render without breaking sometimes,
|
|
||||||
// but typically it works. Better to handle gracefully or redirect.
|
if (!uuid) {
|
||||||
// For now, let's keep it or return 404 UI.
|
|
||||||
// Actually notFound() is for server components mostly.
|
|
||||||
// Let's just return our error UI if ID is invalid.
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
<div className="flex flex-col items-center justify-center min-h-screen">
|
||||||
<h1 className="text-xl font-bold text-red-500">Invalid Correspondence ID</h1>
|
<h1 className="text-xl font-bold text-red-500">Invalid Correspondence UUID</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: correspondence, isLoading, isError } = useCorrespondence(id);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex bg-muted/20 min-h-screen justify-center items-center">
|
<div className="flex bg-muted/20 min-h-screen justify-center items-center">
|
||||||
@@ -33,11 +28,10 @@ export default function CorrespondenceDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !correspondence) {
|
if (isError || !correspondence) {
|
||||||
// Optionally handle 404 vs other errors differently, but for now simple handling
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
<div className="flex flex-col items-center justify-center min-h-screen">
|
||||||
<h1 className="text-xl font-bold text-red-500">Failed to load correspondence</h1>
|
<h1 className="text-xl font-bold text-red-500">Failed to load correspondence</h1>
|
||||||
<p>Please try again later or verify the ID.</p>
|
<p>Please try again later or verify the UUID.</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+8
-6
@@ -1,4 +1,3 @@
|
|||||||
import { drawingApi } from "@/lib/api/drawings";
|
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowLeft, Download, FileText, GitCompare } from "lucide-react";
|
import { ArrowLeft, Download, FileText, GitCompare } from "lucide-react";
|
||||||
@@ -8,19 +7,22 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { RevisionHistory } from "@/components/drawings/revision-history";
|
import { RevisionHistory } from "@/components/drawings/revision-history";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { drawingApi } from "@/lib/api/drawings";
|
||||||
|
|
||||||
export default async function DrawingDetailPage({
|
export default async function DrawingDetailPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ id: string }>;
|
params: Promise<{ uuid: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { id: rawId } = await params;
|
const { uuid } = await params;
|
||||||
const id = parseInt(rawId);
|
if (!uuid) {
|
||||||
if (isNaN(id)) {
|
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const drawing = await drawingApi.getById(id);
|
// TODO: Replace mock drawingApi with real service call using UUID
|
||||||
|
// For now, keep using the mock API with a numeric fallback
|
||||||
|
const drawingId = parseInt(uuid);
|
||||||
|
const drawing = !isNaN(drawingId) ? await drawingApi.getById(drawingId) : undefined;
|
||||||
|
|
||||||
if (!drawing) {
|
if (!drawing) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -104,7 +104,7 @@ export function OrganizationDialog({
|
|||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
updateOrg.mutate(
|
updateOrg.mutate(
|
||||||
{ id: organization.id, data: submitData },
|
{ uuid: organization.uuid, data: submitData },
|
||||||
{ onSuccess: () => onOpenChange(false) }
|
{ onSuccess: () => onOpenChange(false) }
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
|||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
updateUser.mutate(
|
updateUser.mutate(
|
||||||
{ id: user.userId, data: payload },
|
{ uuid: user.uuid, data: payload },
|
||||||
{ onSuccess: () => onOpenChange(false) }
|
{ onSuccess: () => onOpenChange(false) }
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -230,10 +230,13 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
|
|||||||
<SelectValue placeholder="Select Organization" />
|
<SelectValue placeholder="Select Organization" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
{/* TODO: ADR-019 — Backend DTO needs to accept UUID for primaryOrganization.
|
||||||
|
Currently using org.id which is excluded from API responses.
|
||||||
|
Temporary: org.id may still exist in some query responses. */}
|
||||||
{organizations?.map((org: any) => (
|
{organizations?.map((org: any) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={org.id}
|
key={org.uuid ?? org.id}
|
||||||
value={org.id.toString()}
|
value={(org.id ?? 0).toString()}
|
||||||
>
|
>
|
||||||
{org.organizationCode} - {org.organizationName}
|
{org.organizationCode} - {org.organizationName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export function CirculationList({ data }: CirculationListProps) {
|
|||||||
const item = row.original;
|
const item = row.original;
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Link href={`/circulation/${item.id}`}>
|
<Link href={`/circulation/${item.uuid}`}>
|
||||||
<Button variant="ghost" size="icon" title="View Details">
|
<Button variant="ghost" size="icon" title="View Details">
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
|
|||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (confirm("Are you sure you want to submit this correspondence?")) {
|
if (confirm("Are you sure you want to submit this correspondence?")) {
|
||||||
submitMutation.mutate({
|
submitMutation.mutate({
|
||||||
id: data.id,
|
uuid: data.uuid,
|
||||||
data: {}
|
data: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
|
|||||||
|
|
||||||
const action = actionState === "approve" ? "APPROVE" : "REJECT";
|
const action = actionState === "approve" ? "APPROVE" : "REJECT";
|
||||||
processMutation.mutate({
|
processMutation.mutate({
|
||||||
id: data.id,
|
uuid: data.uuid,
|
||||||
data: {
|
data: {
|
||||||
action,
|
action,
|
||||||
comments
|
comments
|
||||||
@@ -83,7 +83,7 @@ export function CorrespondenceDetail({ data }: CorrespondenceDetailProps) {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{/* EDIT BUTTON LOGIC: Show if DRAFT */}
|
{/* EDIT BUTTON LOGIC: Show if DRAFT */}
|
||||||
{status === "DRAFT" && (
|
{status === "DRAFT" && (
|
||||||
<Link href={`/correspondences/${data.id}/edit`}>
|
<Link href={`/correspondences/${data.uuid}/edit`}>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
Edit
|
Edit
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const correspondenceSchema = z.object({
|
|||||||
|
|
||||||
type FormData = z.infer<typeof correspondenceSchema>;
|
type FormData = z.infer<typeof correspondenceSchema>;
|
||||||
|
|
||||||
export function CorrespondenceForm({ initialData, id }: { initialData?: any, id?: number }) {
|
export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, uuid?: string }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const createMutation = useCreateCorrespondence();
|
const createMutation = useCreateCorrespondence();
|
||||||
const updateMutation = useUpdateCorrespondence();
|
const updateMutation = useUpdateCorrespondence();
|
||||||
@@ -107,10 +107,10 @@ export function CorrespondenceForm({ initialData, id }: { initialData?: any, id?
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (id && initialData) {
|
if (uuid && initialData) {
|
||||||
// UPDATE Mode
|
// UPDATE Mode
|
||||||
updateMutation.mutate({ id, data: payload }, {
|
updateMutation.mutate({ uuid, data: payload }, {
|
||||||
onSuccess: () => router.push(`/correspondences/${id}`)
|
onSuccess: () => router.push(`/correspondences/${uuid}`)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// CREATE Mode
|
// CREATE Mode
|
||||||
@@ -420,7 +420,7 @@ export function CorrespondenceForm({ initialData, id }: { initialData?: any, id?
|
|||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isPending}>
|
<Button type="submit" disabled={isPending}>
|
||||||
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
{id ? "Update Correspondence" : "Create Correspondence"}
|
{uuid ? "Update Correspondence" : "Create Correspondence"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -59,15 +59,15 @@ export function CorrespondenceList({ data }: CorrespondenceListProps) {
|
|||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original;
|
const item = row.original;
|
||||||
// Edit/View link goes to the DOCUMENT detail (correspondence.id)
|
// Edit/View link goes to the DOCUMENT detail (correspondence.uuid)
|
||||||
// Ideally we might pass ?revId=item.id to view specific revision, but detail page defaults to latest.
|
// Ideally we might pass ?revId=item.uuid to view specific revision, but detail page defaults to latest.
|
||||||
// For editing, we edit the document.
|
// For editing, we edit the document.
|
||||||
const docId = item.correspondence.id;
|
const docUuid = item.correspondence.uuid;
|
||||||
const statusCode = item.status?.statusCode;
|
const statusCode = item.status?.statusCode;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Link href={`/correspondences/${docId}`}>
|
<Link href={`/correspondences/${docUuid}`}>
|
||||||
<Button variant="ghost" size="icon" title="View Details">
|
<Button variant="ghost" size="icon" title="View Details">
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -89,7 +89,7 @@ export function CorrespondenceList({ data }: CorrespondenceListProps) {
|
|||||||
<FileText className="h-4 w-4" />
|
<FileText className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{statusCode === "DRAFT" && (
|
{statusCode === "DRAFT" && (
|
||||||
<Link href={`/correspondences/${docId}/edit`}>
|
<Link href={`/correspondences/${docUuid}/edit`}>
|
||||||
<Button variant="ghost" size="icon" title="Edit">
|
<Button variant="ghost" size="icon" title="Edit">
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function DrawingCard({ drawing }: { drawing: Drawing }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<Link href={`/drawings/${drawing.drawingId}`}>
|
<Link href={`/drawings/${drawing.uuid}`}>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
<Eye className="mr-2 h-4 w-4" />
|
<Eye className="mr-2 h-4 w-4" />
|
||||||
View
|
View
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import { useSearchSuggestions } from "@/hooks/use-search";
|
|||||||
|
|
||||||
/** Search suggestion item returned from the API */
|
/** Search suggestion item returned from the API */
|
||||||
interface SearchSuggestion {
|
interface SearchSuggestion {
|
||||||
id: string | number;
|
uuid: string;
|
||||||
|
id?: string | number; // Excluded from API responses (ADR-019)
|
||||||
type: string;
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
documentNumber?: string;
|
documentNumber?: string;
|
||||||
@@ -97,12 +98,11 @@ export function GlobalSearch() {
|
|||||||
<CommandGroup heading="Suggestions">
|
<CommandGroup heading="Suggestions">
|
||||||
{(suggestions as SearchSuggestion[]).map((item) => (
|
{(suggestions as SearchSuggestion[]).map((item) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={`${item.type}-${item.id}`}
|
key={`${item.type}-${item.uuid}`}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setQuery(item.title);
|
setQuery(item.title);
|
||||||
// Assumption: item has type and id.
|
// ADR-019: Use UUID for public routes
|
||||||
// If type is missing, we might need a map or check usage in backend response
|
router.push(`/${item.type}s/${item.uuid}`);
|
||||||
router.push(`/${item.type}s/${item.id}`);
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function NotificationsDropdown() {
|
|||||||
|
|
||||||
const handleNotificationClick = (notification: Notification) => {
|
const handleNotificationClick = (notification: Notification) => {
|
||||||
if (!notification.isRead) {
|
if (!notification.isRead) {
|
||||||
markAsRead.mutate(notification.notificationId);
|
markAsRead.mutate(notification.uuid);
|
||||||
}
|
}
|
||||||
if (notification.link) {
|
if (notification.link) {
|
||||||
router.push(notification.link);
|
router.push(notification.link);
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(organizations as Organization[])?.map((org) => (
|
{(organizations as Organization[])?.map((org) => (
|
||||||
<SelectItem key={org.id} value={org.id.toString()}>
|
<SelectItem key={org.uuid} value={(org.id ?? org.uuid).toString()}>
|
||||||
{org.organizationCode} - {org.organizationName}
|
{org.organizationCode} - {org.organizationName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -137,7 +137,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(organizations as Organization[])?.map((org) => (
|
{(organizations as Organization[])?.map((org) => (
|
||||||
<SelectItem key={org.id} value={org.id.toString()}>
|
<SelectItem key={org.uuid} value={(org.id ?? org.uuid).toString()}>
|
||||||
{org.organizationCode} - {org.organizationName}
|
{org.organizationCode} - {org.organizationName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function SearchResults({ results, query, loading }: SearchResultsProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getLink = (result: SearchResult) => {
|
const getLink = (result: SearchResult) => {
|
||||||
return `/${result.type}s/${result.id}`; // Assuming routes are plural (correspondences, rfas, drawings)
|
return `/${result.type}s/${result.uuid}`; // ADR-019: Use UUID for public routes
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -54,7 +54,7 @@ export function SearchResults({ results, query, loading }: SearchResultsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={`${result.type}-${result.id}-${index}`}
|
key={`${result.type}-${result.uuid}-${index}`}
|
||||||
className="p-6 hover:shadow-md transition-shadow group"
|
className="p-6 hover:shadow-md transition-shadow group"
|
||||||
>
|
>
|
||||||
<Link href={getLink(result)}>
|
<Link href={getLink(result)}>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const correspondenceKeys = {
|
|||||||
lists: () => [...correspondenceKeys.all, 'list'] as const,
|
lists: () => [...correspondenceKeys.all, 'list'] as const,
|
||||||
list: (params: SearchCorrespondenceDto) => [...correspondenceKeys.lists(), params] as const,
|
list: (params: SearchCorrespondenceDto) => [...correspondenceKeys.lists(), params] as const,
|
||||||
details: () => [...correspondenceKeys.all, 'detail'] as const,
|
details: () => [...correspondenceKeys.all, 'detail'] as const,
|
||||||
detail: (id: number | string) => [...correspondenceKeys.details(), id] as const,
|
detail: (uuid: string) => [...correspondenceKeys.details(), uuid] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Queries ---
|
// --- Queries ---
|
||||||
@@ -28,11 +28,11 @@ export function useCorrespondences(params: SearchCorrespondenceDto) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCorrespondence(id: number | string) {
|
export function useCorrespondence(uuid: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: correspondenceKeys.detail(id),
|
queryKey: correspondenceKeys.detail(uuid),
|
||||||
queryFn: () => correspondenceService.getById(id),
|
queryFn: () => correspondenceService.getByUuid(uuid),
|
||||||
enabled: !!id,
|
enabled: !!uuid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +59,11 @@ export function useUpdateCorrespondence() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number | string; data: Partial<CreateCorrespondenceDto> }) =>
|
mutationFn: ({ uuid, data }: { uuid: string; data: Partial<CreateCorrespondenceDto> }) =>
|
||||||
correspondenceService.update(id, data),
|
correspondenceService.update(uuid, data),
|
||||||
onSuccess: (_, { id }) => {
|
onSuccess: (_, { uuid }) => {
|
||||||
toast.success('Correspondence updated successfully');
|
toast.success('Correspondence updated successfully');
|
||||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.detail(id) });
|
queryClient.invalidateQueries({ queryKey: correspondenceKeys.detail(uuid) });
|
||||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
||||||
},
|
},
|
||||||
onError: (error: ApiError) => {
|
onError: (error: ApiError) => {
|
||||||
@@ -78,7 +78,7 @@ export function useDeleteCorrespondence() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (id: number | string) => correspondenceService.delete(id),
|
mutationFn: (uuid: string) => correspondenceService.delete(uuid),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Correspondence deleted successfully');
|
toast.success('Correspondence deleted successfully');
|
||||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
||||||
@@ -95,11 +95,11 @@ export function useSubmitCorrespondence() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number; data: SubmitCorrespondenceDto }) =>
|
mutationFn: ({ uuid, data }: { uuid: string; data: SubmitCorrespondenceDto }) =>
|
||||||
correspondenceService.submit(id, data),
|
correspondenceService.submit(uuid, data),
|
||||||
onSuccess: (_, { id }) => {
|
onSuccess: (_, { uuid }) => {
|
||||||
toast.success('Correspondence submitted successfully');
|
toast.success('Correspondence submitted successfully');
|
||||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.detail(id) });
|
queryClient.invalidateQueries({ queryKey: correspondenceKeys.detail(uuid) });
|
||||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
||||||
},
|
},
|
||||||
onError: (error: ApiError) => {
|
onError: (error: ApiError) => {
|
||||||
@@ -114,11 +114,11 @@ export function useProcessWorkflow() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number | string; data: WorkflowActionDto }) =>
|
mutationFn: ({ uuid, data }: { uuid: string; data: WorkflowActionDto }) =>
|
||||||
correspondenceService.processWorkflow(id, data),
|
correspondenceService.processWorkflow(uuid, data),
|
||||||
onSuccess: (_, { id }) => {
|
onSuccess: (_, { uuid }) => {
|
||||||
toast.success('Action completed successfully');
|
toast.success('Action completed successfully');
|
||||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.detail(id) });
|
queryClient.invalidateQueries({ queryKey: correspondenceKeys.detail(uuid) });
|
||||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
||||||
},
|
},
|
||||||
onError: (error: ApiError) => {
|
onError: (error: ApiError) => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const drawingKeys = {
|
|||||||
lists: () => [...drawingKeys.all, 'list'] as const,
|
lists: () => [...drawingKeys.all, 'list'] as const,
|
||||||
list: (type: DrawingType, params: DrawingSearchParams) => [...drawingKeys.lists(), type, params] as const,
|
list: (type: DrawingType, params: DrawingSearchParams) => [...drawingKeys.lists(), type, params] as const,
|
||||||
details: () => [...drawingKeys.all, 'detail'] as const,
|
details: () => [...drawingKeys.all, 'detail'] as const,
|
||||||
detail: (type: DrawingType, id: number | string) => [...drawingKeys.details(), type, id] as const,
|
detail: (type: DrawingType, uuid: string) => [...drawingKeys.details(), type, uuid] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Queries ---
|
// --- Queries ---
|
||||||
@@ -33,7 +33,7 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
|
|||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
const mappedData = response.data.map((d: ContractDrawing) => ({
|
const mappedData = response.data.map((d: ContractDrawing) => ({
|
||||||
...d,
|
...d,
|
||||||
drawingId: d.id,
|
uuid: d.uuid,
|
||||||
drawingNumber: d.contractDrawingNo,
|
drawingNumber: d.contractDrawingNo,
|
||||||
type: 'CONTRACT',
|
type: 'CONTRACT',
|
||||||
}));
|
}));
|
||||||
@@ -46,7 +46,7 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
|
|||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
const mappedData = response.data.map((d: ShopDrawing) => ({
|
const mappedData = response.data.map((d: ShopDrawing) => ({
|
||||||
...d,
|
...d,
|
||||||
drawingId: d.id,
|
uuid: d.uuid,
|
||||||
type: 'SHOP',
|
type: 'SHOP',
|
||||||
title: d.currentRevision?.title || 'Untitled',
|
title: d.currentRevision?.title || 'Untitled',
|
||||||
revision: d.currentRevision?.revisionNumber,
|
revision: d.currentRevision?.revisionNumber,
|
||||||
@@ -61,7 +61,7 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
|
|||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
const mappedData = response.data.map((d: AsBuiltDrawing) => ({
|
const mappedData = response.data.map((d: AsBuiltDrawing) => ({
|
||||||
...d,
|
...d,
|
||||||
drawingId: d.id,
|
uuid: d.uuid,
|
||||||
type: 'AS_BUILT',
|
type: 'AS_BUILT',
|
||||||
title: d.currentRevision?.title || 'Untitled',
|
title: d.currentRevision?.title || 'Untitled',
|
||||||
revision: d.currentRevision?.revisionNumber,
|
revision: d.currentRevision?.revisionNumber,
|
||||||
@@ -76,19 +76,19 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDrawing(type: DrawingType, id: number | string) {
|
export function useDrawing(type: DrawingType, uuid: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: drawingKeys.detail(type, id),
|
queryKey: drawingKeys.detail(type, uuid),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (type === 'CONTRACT') {
|
if (type === 'CONTRACT') {
|
||||||
return contractDrawingService.getById(id);
|
return contractDrawingService.getByUuid(uuid);
|
||||||
} else if (type === 'SHOP') {
|
} else if (type === 'SHOP') {
|
||||||
return shopDrawingService.getById(id);
|
return shopDrawingService.getByUuid(uuid);
|
||||||
} else {
|
} else {
|
||||||
return asBuiltDrawingService.getById(id);
|
return asBuiltDrawingService.getByUuid(uuid);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled: !!id,
|
enabled: !!uuid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export function useCreateOrganization() {
|
|||||||
export function useUpdateOrganization() {
|
export function useUpdateOrganization() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number; data: UpdateOrganizationDto }) =>
|
mutationFn: ({ uuid, data }: { uuid: string; data: UpdateOrganizationDto }) =>
|
||||||
masterDataService.updateOrganization(id, data),
|
masterDataService.updateOrganization(uuid, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Organization updated successfully');
|
toast.success('Organization updated successfully');
|
||||||
queryClient.invalidateQueries({ queryKey: masterDataKeys.organizations() });
|
queryClient.invalidateQueries({ queryKey: masterDataKeys.organizations() });
|
||||||
@@ -61,7 +61,7 @@ export function useUpdateOrganization() {
|
|||||||
export function useDeleteOrganization() {
|
export function useDeleteOrganization() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (id: number) => masterDataService.deleteOrganization(id),
|
mutationFn: (uuid: string) => masterDataService.deleteOrganization(uuid),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success('Organization deleted successfully');
|
toast.success('Organization deleted successfully');
|
||||||
queryClient.invalidateQueries({ queryKey: masterDataKeys.organizations() });
|
queryClient.invalidateQueries({ queryKey: masterDataKeys.organizations() });
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getApiErrorMessage } from '@/types/api-error';
|
|||||||
export const projectKeys = {
|
export const projectKeys = {
|
||||||
all: ['projects'] as const,
|
all: ['projects'] as const,
|
||||||
list: (params: SearchProjectDto) => [...projectKeys.all, 'list', params] as const,
|
list: (params: SearchProjectDto) => [...projectKeys.all, 'list', params] as const,
|
||||||
detail: (id: number) => [...projectKeys.all, 'detail', id] as const,
|
detail: (uuid: string) => [...projectKeys.all, 'detail', uuid] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useProjects(params?: SearchProjectDto) {
|
export function useProjects(params?: SearchProjectDto) {
|
||||||
@@ -36,7 +36,7 @@ export function useCreateProject() {
|
|||||||
export function useUpdateProject() {
|
export function useUpdateProject() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number; data: UpdateProjectDto }) => projectService.update(id, data),
|
mutationFn: ({ uuid, data }: { uuid: string; data: UpdateProjectDto }) => projectService.update(uuid, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Project updated successfully");
|
toast.success("Project updated successfully");
|
||||||
queryClient.invalidateQueries({ queryKey: projectKeys.all });
|
queryClient.invalidateQueries({ queryKey: projectKeys.all });
|
||||||
@@ -52,7 +52,7 @@ export function useUpdateProject() {
|
|||||||
export function useDeleteProject() {
|
export function useDeleteProject() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (id: number) => projectService.delete(id),
|
mutationFn: (uuid: string) => projectService.delete(uuid),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Project deleted successfully");
|
toast.success("Project deleted successfully");
|
||||||
queryClient.invalidateQueries({ queryKey: projectKeys.all });
|
queryClient.invalidateQueries({ queryKey: projectKeys.all });
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getApiErrorMessage } from '@/types/api-error';
|
|||||||
export const userKeys = {
|
export const userKeys = {
|
||||||
all: ['users'] as const,
|
all: ['users'] as const,
|
||||||
list: (params?: SearchUserDto) => [...userKeys.all, 'list', params] as const,
|
list: (params?: SearchUserDto) => [...userKeys.all, 'list', params] as const,
|
||||||
detail: (id: number) => [...userKeys.all, 'detail', id] as const,
|
detail: (uuid: string) => [...userKeys.all, 'detail', uuid] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useUsers(params?: SearchUserDto) {
|
export function useUsers(params?: SearchUserDto) {
|
||||||
@@ -43,7 +43,7 @@ export function useCreateUser() {
|
|||||||
export function useUpdateUser() {
|
export function useUpdateUser() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number; data: UpdateUserDto }) => userService.update(id, data),
|
mutationFn: ({ uuid, data }: { uuid: string; data: UpdateUserDto }) => userService.update(uuid, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("User updated successfully");
|
toast.success("User updated successfully");
|
||||||
queryClient.invalidateQueries({ queryKey: userKeys.all });
|
queryClient.invalidateQueries({ queryKey: userKeys.all });
|
||||||
@@ -59,7 +59,7 @@ export function useUpdateUser() {
|
|||||||
export function useDeleteUser() {
|
export function useDeleteUser() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (id: number) => userService.delete(id),
|
mutationFn: (uuid: string) => userService.delete(uuid),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("User deleted successfully");
|
toast.success("User deleted successfully");
|
||||||
queryClient.invalidateQueries({ queryKey: userKeys.all });
|
queryClient.invalidateQueries({ queryKey: userKeys.all });
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { NotificationResponse } from "@/types/notification";
|
|||||||
// Mock Data
|
// Mock Data
|
||||||
let mockNotifications = [
|
let mockNotifications = [
|
||||||
{
|
{
|
||||||
|
uuid: "019575a0-0001-7000-8000-000000000001",
|
||||||
notificationId: 1,
|
notificationId: 1,
|
||||||
title: "RFA Approved",
|
title: "RFA Approved",
|
||||||
message: "RFA-001 has been approved by the Project Manager.",
|
message: "RFA-001 has been approved by the Project Manager.",
|
||||||
@@ -12,6 +13,7 @@ let mockNotifications = [
|
|||||||
link: "/rfas/1",
|
link: "/rfas/1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
uuid: "019575a0-0002-7000-8000-000000000002",
|
||||||
notificationId: 2,
|
notificationId: 2,
|
||||||
title: "New Correspondence",
|
title: "New Correspondence",
|
||||||
message: "You have received a new correspondence from Contractor A.",
|
message: "You have received a new correspondence from Contractor A.",
|
||||||
@@ -21,6 +23,7 @@ let mockNotifications = [
|
|||||||
link: "/correspondences/3",
|
link: "/correspondences/3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
uuid: "019575a0-0003-7000-8000-000000000003",
|
||||||
notificationId: 3,
|
notificationId: 3,
|
||||||
title: "Drawing Revision Required",
|
title: "Drawing Revision Required",
|
||||||
message: "Drawing S-201 requires revision based on recent comments.",
|
message: "Drawing S-201 requires revision based on recent comments.",
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export const asBuiltDrawingService = {
|
|||||||
/**
|
/**
|
||||||
* Get details by ID
|
* Get details by ID
|
||||||
*/
|
*/
|
||||||
getById: async (id: string | number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
const response = await apiClient.get(`/drawings/asbuilt/${id}`);
|
const response = await apiClient.get(`/drawings/asbuilt/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ export const asBuiltDrawingService = {
|
|||||||
/**
|
/**
|
||||||
* Create New Revision
|
* Create New Revision
|
||||||
*/
|
*/
|
||||||
createRevision: async (id: string | number, data: CreateAsBuiltDrawingRevisionDto) => {
|
createRevision: async (uuid: string, data: CreateAsBuiltDrawingRevisionDto) => {
|
||||||
const response = await apiClient.post(`/drawings/asbuilt/${id}/revisions`, data);
|
const response = await apiClient.post(`/drawings/asbuilt/${uuid}/revisions`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export const circulationService = {
|
|||||||
/**
|
/**
|
||||||
* ดึงรายละเอียดใบเวียนตาม ID
|
* ดึงรายละเอียดใบเวียนตาม ID
|
||||||
*/
|
*/
|
||||||
getById: async (id: string | number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
// GET /circulations/:id
|
// GET /circulations/:uuid
|
||||||
const response = await apiClient.get(`/circulations/${id}`);
|
const response = await apiClient.get(`/circulations/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -47,8 +47,8 @@ export const circulationService = {
|
|||||||
/**
|
/**
|
||||||
* ลบ/ยกเลิกใบเวียน
|
* ลบ/ยกเลิกใบเวียน
|
||||||
*/
|
*/
|
||||||
delete: async (id: string | number) => {
|
delete: async (uuid: string) => {
|
||||||
const response = await apiClient.delete(`/circulations/${id}`);
|
const response = await apiClient.delete(`/circulations/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export const contractDrawingService = {
|
|||||||
/**
|
/**
|
||||||
* ดึงรายละเอียดตาม ID
|
* ดึงรายละเอียดตาม ID
|
||||||
*/
|
*/
|
||||||
getById: async (id: string | number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
const response = await apiClient.get(`/drawings/contract/${id}`);
|
const response = await apiClient.get(`/drawings/contract/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -33,16 +33,16 @@ export const contractDrawingService = {
|
|||||||
/**
|
/**
|
||||||
* แก้ไขข้อมูลแบบสัญญา
|
* แก้ไขข้อมูลแบบสัญญา
|
||||||
*/
|
*/
|
||||||
update: async (id: string | number, data: UpdateContractDrawingDto) => {
|
update: async (uuid: string, data: UpdateContractDrawingDto) => {
|
||||||
const response = await apiClient.put(`/drawings/contract/${id}`, data);
|
const response = await apiClient.put(`/drawings/contract/${uuid}`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ลบแบบสัญญา (Soft Delete)
|
* ลบแบบสัญญา (Soft Delete)
|
||||||
*/
|
*/
|
||||||
delete: async (id: string | number) => {
|
delete: async (uuid: string) => {
|
||||||
const response = await apiClient.delete(`/drawings/contract/${id}`);
|
const response = await apiClient.delete(`/drawings/contract/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ export const contractService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get contract by ID
|
* Get contract by UUID
|
||||||
* GET /contracts/:id
|
* GET /contracts/:uuid
|
||||||
*/
|
*/
|
||||||
getById: async (id: number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
const response = await apiClient.get(`/contracts/${id}`);
|
const response = await apiClient.get(`/contracts/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -38,19 +38,19 @@ export const contractService = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update contract
|
* Update contract
|
||||||
* PATCH /contracts/:id
|
* PATCH /contracts/:uuid
|
||||||
*/
|
*/
|
||||||
update: async (id: number, data: UpdateContractDto) => {
|
update: async (uuid: string, data: UpdateContractDto) => {
|
||||||
const response = await apiClient.patch(`/contracts/${id}`, data);
|
const response = await apiClient.patch(`/contracts/${uuid}`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete contract
|
* Delete contract
|
||||||
* DELETE /contracts/:id
|
* DELETE /contracts/:uuid
|
||||||
*/
|
*/
|
||||||
delete: async (id: number) => {
|
delete: async (uuid: string) => {
|
||||||
const response = await apiClient.delete(`/contracts/${id}`);
|
const response = await apiClient.delete(`/contracts/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ export const correspondenceService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getById: async (id: string | number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
const response = await apiClient.get(`/correspondences/${id}`);
|
const response = await apiClient.get(`/correspondences/${uuid}`);
|
||||||
return response.data.data; // Unwrap NestJS Interceptor 'data' wrapper
|
return response.data.data; // Unwrap NestJS Interceptor 'data' wrapper
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -25,13 +25,13 @@ export const correspondenceService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (id: string | number, data: Partial<CreateCorrespondenceDto>) => {
|
update: async (uuid: string, data: Partial<CreateCorrespondenceDto>) => {
|
||||||
const response = await apiClient.put(`/correspondences/${id}`, data);
|
const response = await apiClient.put(`/correspondences/${uuid}`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async (id: string | number) => {
|
delete: async (uuid: string) => {
|
||||||
const response = await apiClient.delete(`/correspondences/${id}`);
|
const response = await apiClient.delete(`/correspondences/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -40,33 +40,33 @@ export const correspondenceService = {
|
|||||||
/**
|
/**
|
||||||
* ส่งเอกสาร (Submit) เพื่อเริ่ม Workflow
|
* ส่งเอกสาร (Submit) เพื่อเริ่ม Workflow
|
||||||
*/
|
*/
|
||||||
submit: async (id: string | number, data: SubmitCorrespondenceDto) => {
|
submit: async (uuid: string, data: SubmitCorrespondenceDto) => {
|
||||||
const response = await apiClient.post(`/correspondences/${id}/submit`, data);
|
const response = await apiClient.post(`/correspondences/${uuid}/submit`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ดำเนินการ Workflow (เช่น Approve, Reject) ในขั้นตอนปัจจุบัน
|
* ดำเนินการ Workflow (เช่น Approve, Reject) ในขั้นตอนปัจจุบัน
|
||||||
*/
|
*/
|
||||||
processWorkflow: async (id: string | number, data: WorkflowActionDto) => {
|
processWorkflow: async (uuid: string, data: WorkflowActionDto) => {
|
||||||
const response = await apiClient.post(`/correspondences/${id}/workflow`, data);
|
const response = await apiClient.post(`/correspondences/${uuid}/workflow`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* เพิ่มเอกสารอ้างอิง
|
* เพิ่มเอกสารอ้างอิง
|
||||||
*/
|
*/
|
||||||
addReference: async (id: string | number, data: AddReferenceDto) => {
|
addReference: async (uuid: string, data: AddReferenceDto) => {
|
||||||
const response = await apiClient.post(`/correspondences/${id}/references`, data);
|
const response = await apiClient.post(`/correspondences/${uuid}/references`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ลบเอกสารอ้างอิง
|
* ลบเอกสารอ้างอิง
|
||||||
*/
|
*/
|
||||||
removeReference: async (id: string | number, data: RemoveReferenceDto) => {
|
removeReference: async (uuid: string, data: RemoveReferenceDto) => {
|
||||||
// ใช้ DELETE method โดยส่ง body ไปด้วย (axios รองรับผ่าน config.data)
|
// ใช้ DELETE method โดยส่ง body ไปด้วย (axios รองรับผ่าน config.data)
|
||||||
const response = await apiClient.delete(`/correspondences/${id}/references`, {
|
const response = await apiClient.delete(`/correspondences/${uuid}/references`, {
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -78,14 +78,14 @@ export const masterDataService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/** แก้ไของค์กร */
|
/** แก้ไของค์กร */
|
||||||
updateOrganization: async (id: number, data: UpdateOrganizationDto) => {
|
updateOrganization: async (uuid: string, data: UpdateOrganizationDto) => {
|
||||||
const response = await apiClient.put(`/organizations/${id}`, data);
|
const response = await apiClient.put(`/organizations/${uuid}`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** ลบองค์กร */
|
/** ลบองค์กร */
|
||||||
deleteOrganization: async (id: number) => {
|
deleteOrganization: async (uuid: string) => {
|
||||||
const response = await apiClient.delete(`/organizations/${id}`);
|
const response = await apiClient.delete(`/organizations/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export const notificationService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
markAsRead: async (id: number) => {
|
markAsRead: async (uuid: string) => {
|
||||||
const response = await apiClient.patch(`/notifications/${id}/read`);
|
const response = await apiClient.put(`/notifications/${uuid}/read`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ export const organizationService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get organization by ID
|
* Get organization by UUID
|
||||||
* GET /organizations/:id
|
* GET /organizations/:uuid
|
||||||
*/
|
*/
|
||||||
getById: async (id: number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
const response = await apiClient.get(`/organizations/${id}`);
|
const response = await apiClient.get(`/organizations/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -39,19 +39,19 @@ export const organizationService = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update organization
|
* Update organization
|
||||||
* PATCH /organizations/:id
|
* PATCH /organizations/:uuid
|
||||||
*/
|
*/
|
||||||
update: async (id: number, data: UpdateOrganizationDto) => {
|
update: async (uuid: string, data: UpdateOrganizationDto) => {
|
||||||
const response = await apiClient.patch(`/organizations/${id}`, data);
|
const response = await apiClient.patch(`/organizations/${uuid}`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete organization
|
* Delete organization
|
||||||
* DELETE /organizations/:id
|
* DELETE /organizations/:uuid
|
||||||
*/
|
*/
|
||||||
delete: async (id: number) => {
|
delete: async (uuid: string) => {
|
||||||
const response = await apiClient.delete(`/organizations/${id}`);
|
const response = await apiClient.delete(`/organizations/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ export const projectService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** ดึงรายละเอียดโครงการตาม ID */
|
/** ดึงรายละเอียดโครงการตาม UUID */
|
||||||
getById: async (id: string | number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
const response = await apiClient.get(`/projects/${id}`);
|
const response = await apiClient.get(`/projects/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -36,14 +36,14 @@ export const projectService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/** แก้ไขโครงการ */
|
/** แก้ไขโครงการ */
|
||||||
update: async (id: string | number, data: UpdateProjectDto) => {
|
update: async (uuid: string, data: UpdateProjectDto) => {
|
||||||
const response = await apiClient.put(`/projects/${id}`, data);
|
const response = await apiClient.put(`/projects/${uuid}`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** ลบโครงการ (Soft Delete) */
|
/** ลบโครงการ (Soft Delete) */
|
||||||
delete: async (id: string | number) => {
|
delete: async (uuid: string) => {
|
||||||
const response = await apiClient.delete(`/projects/${id}`);
|
const response = await apiClient.delete(`/projects/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export const shopDrawingService = {
|
|||||||
/**
|
/**
|
||||||
* ดึงรายละเอียดตาม ID (ควรได้ Revision History มาด้วย)
|
* ดึงรายละเอียดตาม ID (ควรได้ Revision History มาด้วย)
|
||||||
*/
|
*/
|
||||||
getById: async (id: string | number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
const response = await apiClient.get(`/drawings/shop/${id}`);
|
const response = await apiClient.get(`/drawings/shop/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ export const shopDrawingService = {
|
|||||||
/**
|
/**
|
||||||
* สร้าง Revision ใหม่สำหรับ Shop Drawing เดิม
|
* สร้าง Revision ใหม่สำหรับ Shop Drawing เดิม
|
||||||
*/
|
*/
|
||||||
createRevision: async (id: string | number, data: CreateShopDrawingRevisionDto) => {
|
createRevision: async (uuid: string, data: CreateShopDrawingRevisionDto) => {
|
||||||
const response = await apiClient.post(`/drawings/shop/${id}/revisions`, data);
|
const response = await apiClient.post(`/drawings/shop/${uuid}/revisions`, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ interface RawUser {
|
|||||||
const transformUser = (user: RawUser): User => {
|
const transformUser = (user: RawUser): User => {
|
||||||
return {
|
return {
|
||||||
...(user as unknown as User),
|
...(user as unknown as User),
|
||||||
userId: (user.user_id ?? user.userId) as number,
|
uuid: (user.uuid as string) ?? '',
|
||||||
|
userId: (user.user_id ?? user.userId) as number | undefined,
|
||||||
roles: (user.assignments?.map((a) => a.role) ?? []) as User['roles'],
|
roles: (user.assignments?.map((a) => a.role) ?? []) as User['roles'],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -45,8 +46,8 @@ export const userService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
getById: async (id: number) => {
|
getByUuid: async (uuid: string) => {
|
||||||
const response = await apiClient.get<RawUser>(`/users/${id}`);
|
const response = await apiClient.get<RawUser>(`/users/${uuid}`);
|
||||||
return transformUser(response.data);
|
return transformUser(response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -55,13 +56,13 @@ export const userService = {
|
|||||||
return transformUser(response.data);
|
return transformUser(response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (id: number, data: UpdateUserDto) => {
|
update: async (uuid: string, data: UpdateUserDto) => {
|
||||||
const response = await apiClient.put<RawUser>(`/users/${id}`, data);
|
const response = await apiClient.put<RawUser>(`/users/${uuid}`, data);
|
||||||
return transformUser(response.data);
|
return transformUser(response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async (id: number) => {
|
delete: async (uuid: string) => {
|
||||||
const response = await apiClient.delete(`/users/${id}`);
|
const response = await apiClient.delete(`/users/${uuid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ export interface CirculationRouting {
|
|||||||
* Main Circulation entity
|
* Main Circulation entity
|
||||||
*/
|
*/
|
||||||
export interface Circulation {
|
export interface Circulation {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
correspondenceId?: number;
|
correspondenceId?: number;
|
||||||
organizationId: number;
|
organizationId: number;
|
||||||
circulationNo: string;
|
circulationNo: string;
|
||||||
@@ -52,16 +53,19 @@ export interface Circulation {
|
|||||||
// Joined relations from API
|
// Joined relations from API
|
||||||
routings?: CirculationRouting[];
|
routings?: CirculationRouting[];
|
||||||
correspondence?: {
|
correspondence?: {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number;
|
||||||
correspondence_number: string;
|
correspondence_number: string;
|
||||||
};
|
};
|
||||||
organization?: {
|
organization?: {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number;
|
||||||
organization_code: string;
|
organization_code: string;
|
||||||
organization_name: string;
|
organization_name: string;
|
||||||
};
|
};
|
||||||
creator?: {
|
creator?: {
|
||||||
user_id: number;
|
uuid: string;
|
||||||
|
user_id?: number;
|
||||||
username: string;
|
username: string;
|
||||||
first_name?: string;
|
first_name?: string;
|
||||||
last_name?: string;
|
last_name?: string;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
export interface Organization {
|
export interface Organization {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
organizationName: string;
|
organizationName: string;
|
||||||
organizationCode: string;
|
organizationCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Attachment {
|
export interface Attachment {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
@@ -15,7 +17,8 @@ export interface Attachment {
|
|||||||
|
|
||||||
// Used in List View mainly
|
// Used in List View mainly
|
||||||
export interface CorrespondenceRevision {
|
export interface CorrespondenceRevision {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
revisionNumber: number;
|
revisionNumber: number;
|
||||||
revisionLabel?: string; // e.g. "A", "00"
|
revisionLabel?: string; // e.g. "A", "00"
|
||||||
subject: string;
|
subject: string;
|
||||||
@@ -36,20 +39,22 @@ export interface CorrespondenceRevision {
|
|||||||
|
|
||||||
// Nested Relation from Backend Refactor
|
// Nested Relation from Backend Refactor
|
||||||
correspondence: {
|
correspondence: {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
originatorId?: number;
|
originatorId?: number;
|
||||||
isInternal: boolean;
|
isInternal: boolean;
|
||||||
originator?: Organization;
|
originator?: Organization;
|
||||||
project?: { id: number; projectName: string; projectCode: string };
|
project?: { uuid: string; id?: number; projectName: string; projectCode: string };
|
||||||
type?: { id: number; typeName: string; typeCode: string };
|
type?: { id: number; typeName: string; typeCode: string };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep explicit Correspondence for Detail View if needed, or merge concepts
|
// Keep explicit Correspondence for Detail View if needed, or merge concepts
|
||||||
export interface Correspondence {
|
export interface Correspondence {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
originatorId?: number;
|
originatorId?: number;
|
||||||
@@ -59,7 +64,7 @@ export interface Correspondence {
|
|||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
originator?: Organization;
|
originator?: Organization;
|
||||||
project?: { id: number; projectName: string; projectCode: string };
|
project?: { uuid: string; id?: number; projectName: string; projectCode: string };
|
||||||
type?: { id: number; typeName: string; typeCode: string };
|
type?: { id: number; typeName: string; typeCode: string };
|
||||||
revisions?: CorrespondenceRevision[]; // Nested revisions
|
revisions?: CorrespondenceRevision[]; // Nested revisions
|
||||||
recipients?: {
|
recipients?: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Entity Interfaces
|
// Entity Interfaces
|
||||||
export interface DrawingRevision {
|
export interface DrawingRevision {
|
||||||
revisionId: number;
|
uuid: string;
|
||||||
|
revisionId?: number; // Excluded from API responses (ADR-019)
|
||||||
revisionNumber: string;
|
revisionNumber: string;
|
||||||
title?: string; // Added
|
title?: string; // Added
|
||||||
legacyDrawingNumber?: string; // Added
|
legacyDrawingNumber?: string; // Added
|
||||||
@@ -14,7 +15,8 @@ export interface DrawingRevision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ContractDrawing {
|
export interface ContractDrawing {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
contractDrawingNo: string;
|
contractDrawingNo: string;
|
||||||
title: string;
|
title: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
@@ -26,7 +28,8 @@ export interface ContractDrawing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ShopDrawing {
|
export interface ShopDrawing {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
drawingNumber: string;
|
drawingNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
mainCategoryId: number;
|
mainCategoryId: number;
|
||||||
@@ -38,7 +41,8 @@ export interface ShopDrawing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AsBuiltDrawing {
|
export interface AsBuiltDrawing {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
drawingNumber: string;
|
drawingNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
mainCategoryId: number;
|
mainCategoryId: number;
|
||||||
@@ -50,7 +54,8 @@ export interface AsBuiltDrawing {
|
|||||||
|
|
||||||
// Unified Type for List
|
// Unified Type for List
|
||||||
export interface Drawing {
|
export interface Drawing {
|
||||||
drawingId: number;
|
uuid?: string;
|
||||||
|
drawingId?: number; // Excluded from API responses (ADR-019)
|
||||||
drawingNumber: string;
|
drawingNumber: string;
|
||||||
title: string; // Display title (from current revision for Shop/AsBuilt)
|
title: string; // Display title (from current revision for Shop/AsBuilt)
|
||||||
discipline?: string | { disciplineCode: string; disciplineName: string };
|
discipline?: string | { disciplineCode: string; disciplineName: string };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface Notification {
|
export interface Notification {
|
||||||
notificationId: number;
|
uuid: string;
|
||||||
|
notificationId?: number; // Excluded from API responses (ADR-019)
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
type: "INFO" | "SUCCESS" | "WARNING" | "ERROR";
|
type: "INFO" | "SUCCESS" | "WARNING" | "ERROR";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface Organization {
|
export interface Organization {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
organizationCode: string;
|
organizationCode: string;
|
||||||
organizationName: string;
|
organizationName: string;
|
||||||
roleId?: number; // NEW - organization role (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD_PARTY)
|
roleId?: number; // NEW - organization role (OWNER, DESIGNER, CONSULTANT, CONTRACTOR, THIRD_PARTY)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface SearchResult {
|
export interface SearchResult {
|
||||||
id: number;
|
uuid: string;
|
||||||
|
id?: number; // Excluded from API responses (ADR-019)
|
||||||
type: "correspondence" | "rfa" | "drawing";
|
type: "correspondence" | "rfa" | "drawing";
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export interface UserOrganization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
userId: number;
|
uuid: string;
|
||||||
|
userId?: number; // Excluded from API responses (ADR-019)
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
// EDITOR SETTINGS
|
// EDITOR SETTINGS
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
"editor.fontSize": 18,
|
"editor.fontSize": 20,
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.lineHeight": 1.6,
|
"editor.lineHeight": 1.6,
|
||||||
"editor.rulers": [80, 120],
|
"editor.rulers": [80, 120],
|
||||||
@@ -636,7 +636,7 @@
|
|||||||
// DEBUGGING
|
// DEBUGGING
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
"debug.console.fontSize": 14,
|
"debug.console.fontSize": 16,
|
||||||
"debug.console.fontFamily": "Consolas, 'Courier New', monospace",
|
"debug.console.fontFamily": "Consolas, 'Courier New', monospace",
|
||||||
"debug.console.lineHeight": 20,
|
"debug.console.lineHeight": 20,
|
||||||
"debug.console.wordWrap": false,
|
"debug.console.wordWrap": false,
|
||||||
@@ -670,6 +670,11 @@
|
|||||||
"vitest.enable": true,
|
"vitest.enable": true,
|
||||||
"yaml.maxItemsComputed": 6000,
|
"yaml.maxItemsComputed": 6000,
|
||||||
"powershell.cwd": "🎯 Root",
|
"powershell.cwd": "🎯 Root",
|
||||||
|
"files.autoSave": "onFocusChange",
|
||||||
|
"diffEditor.codeLens": false,
|
||||||
|
"workbench.colorTheme": "Default Dark Modern",
|
||||||
|
"workbench.preferredDarkColorTheme": "Default Dark Modern",
|
||||||
|
"scm.alwaysShowActions": false,
|
||||||
},
|
},
|
||||||
// ========================================
|
// ========================================
|
||||||
// LAUNCH CONFIGURATIONS
|
// LAUNCH CONFIGURATIONS
|
||||||
|
|||||||
Generated
+24
-30
@@ -167,8 +167,8 @@ importers:
|
|||||||
specifier: ^0.3.27
|
specifier: ^0.3.27
|
||||||
version: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
version: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^9.0.1
|
specifier: ^11.1.0
|
||||||
version: 9.0.1
|
version: 11.1.0
|
||||||
winston:
|
winston:
|
||||||
specifier: ^3.18.3
|
specifier: ^3.18.3
|
||||||
version: 3.18.3
|
version: 3.18.3
|
||||||
@@ -178,7 +178,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@compodoc/compodoc':
|
'@compodoc/compodoc':
|
||||||
specifier: ^1.1.32
|
specifier: ^1.1.32
|
||||||
version: 1.1.32(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(typescript@5.9.3)(vis-data@8.0.3(uuid@9.0.1)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
version: 1.1.32(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(typescript@5.9.3)(vis-data@8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
version: 3.3.1
|
version: 3.3.1
|
||||||
@@ -231,8 +231,8 @@ importers:
|
|||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
'@types/uuid':
|
'@types/uuid':
|
||||||
specifier: ^9.0.8
|
specifier: ^10.0.0
|
||||||
version: 9.0.8
|
version: 10.0.0
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.18.0
|
specifier: ^9.18.0
|
||||||
version: 9.39.1(jiti@1.21.7)
|
version: 9.39.1(jiti@1.21.7)
|
||||||
@@ -3797,13 +3797,13 @@ packages:
|
|||||||
'@types/trusted-types@2.0.7':
|
'@types/trusted-types@2.0.7':
|
||||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
|
'@types/uuid@10.0.0':
|
||||||
|
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
|
||||||
|
|
||||||
'@types/uuid@11.0.0':
|
'@types/uuid@11.0.0':
|
||||||
resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==}
|
resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==}
|
||||||
deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.
|
deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.
|
||||||
|
|
||||||
'@types/uuid@9.0.8':
|
|
||||||
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
|
|
||||||
|
|
||||||
'@types/validator@13.15.10':
|
'@types/validator@13.15.10':
|
||||||
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
|
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
|
||||||
|
|
||||||
@@ -8125,10 +8125,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
uuid@9.0.1:
|
|
||||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
v8-compile-cache-lib@3.0.1:
|
v8-compile-cache-lib@3.0.1:
|
||||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||||
|
|
||||||
@@ -9777,7 +9773,7 @@ snapshots:
|
|||||||
|
|
||||||
'@colors/colors@1.6.0': {}
|
'@colors/colors@1.6.0': {}
|
||||||
|
|
||||||
'@compodoc/compodoc@1.1.32(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(typescript@5.9.3)(vis-data@8.0.3(uuid@9.0.1)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))':
|
'@compodoc/compodoc@1.1.32(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(typescript@5.9.3)(vis-data@8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular-devkit/schematics': 20.3.4(chokidar@4.0.3)
|
'@angular-devkit/schematics': 20.3.4(chokidar@4.0.3)
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.4
|
||||||
@@ -9821,7 +9817,7 @@ snapshots:
|
|||||||
tablesort: 5.6.0
|
tablesort: 5.6.0
|
||||||
ts-morph: 27.0.2
|
ts-morph: 27.0.2
|
||||||
uuid: 11.1.0
|
uuid: 11.1.0
|
||||||
vis-network: 10.0.2(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@11.1.0)(vis-data@8.0.3(uuid@9.0.1)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
vis-network: 10.0.2(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@11.1.0)(vis-data@8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@egjs/hammerjs'
|
- '@egjs/hammerjs'
|
||||||
- component-emitter
|
- component-emitter
|
||||||
@@ -12411,12 +12407,12 @@ snapshots:
|
|||||||
'@types/trusted-types@2.0.7':
|
'@types/trusted-types@2.0.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/uuid@10.0.0': {}
|
||||||
|
|
||||||
'@types/uuid@11.0.0':
|
'@types/uuid@11.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
uuid: 13.0.0
|
uuid: 13.0.0
|
||||||
|
|
||||||
'@types/uuid@9.0.8': {}
|
|
||||||
|
|
||||||
'@types/validator@13.15.10': {}
|
'@types/validator@13.15.10': {}
|
||||||
|
|
||||||
'@types/yargs-parser@21.0.3': {}
|
'@types/yargs-parser@21.0.3': {}
|
||||||
@@ -14000,8 +13996,8 @@ snapshots:
|
|||||||
'@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||||
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
|
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
|
||||||
@@ -14024,7 +14020,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -14035,22 +14031,22 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3)
|
'@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -14061,7 +14057,7 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -17389,8 +17385,6 @@ snapshots:
|
|||||||
|
|
||||||
uuid@8.3.2: {}
|
uuid@8.3.2: {}
|
||||||
|
|
||||||
uuid@9.0.1: {}
|
|
||||||
|
|
||||||
v8-compile-cache-lib@3.0.1: {}
|
v8-compile-cache-lib@3.0.1: {}
|
||||||
|
|
||||||
v8-to-istanbul@9.3.0:
|
v8-to-istanbul@9.3.0:
|
||||||
@@ -17403,18 +17397,18 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vis-data@8.0.3(uuid@9.0.1)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)):
|
vis-data@8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
uuid: 9.0.1
|
uuid: 11.1.0
|
||||||
vis-util: 6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)
|
vis-util: 6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)
|
||||||
|
|
||||||
vis-network@10.0.2(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@11.1.0)(vis-data@8.0.3(uuid@9.0.1)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)):
|
vis-network@10.0.2(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@11.1.0)(vis-data@8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@egjs/hammerjs': 2.0.17
|
'@egjs/hammerjs': 2.0.17
|
||||||
component-emitter: 1.3.1
|
component-emitter: 1.3.1
|
||||||
keycharm: 0.4.0
|
keycharm: 0.4.0
|
||||||
uuid: 11.1.0
|
uuid: 11.1.0
|
||||||
vis-data: 8.0.3(uuid@9.0.1)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
vis-data: 8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
||||||
vis-util: 6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)
|
vis-util: 6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)
|
||||||
|
|
||||||
vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1):
|
vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1):
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ UNIQUE | Role name (
|
|||||||
|
|
||||||
* * Purpose **: MASTER TABLE storing ALL organizations involved IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ----------------- | ------------ | ----------------------------------- | ---------------------------------------- |
|
* * Purpose **: MASTER TABLE storing ALL organizations involved IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ----------------- | ------------ | ----------------------------------- | ---------------------------------------- |
|
||||||
| id | INT | PRIMARY KEY,
|
| id | INT | PRIMARY KEY,
|
||||||
AUTO_INCREMENT | UNIQUE identifier FOR organization | | organization_code | VARCHAR(20) | NOT NULL,
|
AUTO_INCREMENT | UNIQUE identifier FOR organization | | uuid | UUID | NOT NULL, UNIQUE, DEFAULT UUID() | UUID Public Identifier (ADR-019) | | organization_code | VARCHAR(20) | NOT NULL,
|
||||||
UNIQUE | Organization code (e.g., 'กทท.', 'TEAM') | | organization_name | VARCHAR(255) | NOT NULL | FULL organization name | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS (1 = active, 0 = inactive) | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last
|
UNIQUE | Organization code (e.g., 'กทท.', 'TEAM') | | organization_name | VARCHAR(255) | NOT NULL | FULL organization name | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS (1 = active, 0 = inactive) | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last
|
||||||
UPDATE timestamp |
|
UPDATE timestamp |
|
||||||
| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (organization_code) - INDEX (is_active) ** Relationships **: - Referenced by: users,
|
| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (organization_code) - INDEX (is_active) ** Relationships **: - Referenced by: users,
|
||||||
@@ -117,7 +117,7 @@ UPDATE timestamp |
|
|||||||
|
|
||||||
* * Purpose **: MASTER TABLE FOR ALL projects IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ------------ | ------------ | --------------------------- | ----------------------------- |
|
* * Purpose **: MASTER TABLE FOR ALL projects IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ------------ | ------------ | --------------------------- | ----------------------------- |
|
||||||
| id | INT | PRIMARY KEY,
|
| id | INT | PRIMARY KEY,
|
||||||
AUTO_INCREMENT | UNIQUE identifier FOR project | | project_code | VARCHAR(50) | NOT NULL,
|
AUTO_INCREMENT | UNIQUE identifier FOR project | | uuid | UUID | NOT NULL, UNIQUE, DEFAULT UUID() | UUID Public Identifier (ADR-019) | | project_code | VARCHAR(50) | NOT NULL,
|
||||||
UNIQUE | Project code (e.g., 'LCBP3') | | project_name | VARCHAR(255) | NOT NULL | FULL project name | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS |
|
UNIQUE | Project code (e.g., 'LCBP3') | | project_name | VARCHAR(255) | NOT NULL | FULL project name | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS |
|
||||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
|
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
|
||||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
|
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
|
||||||
@@ -131,7 +131,7 @@ UPDATE timestamp |
|
|||||||
|
|
||||||
* * Purpose **: MASTER TABLE FOR contracts within projects | COLUMN Name | Data TYPE | Constraints | Description | | ------------- | ------------ | ----------------------------------- | ------------------------------ |
|
* * Purpose **: MASTER TABLE FOR contracts within projects | COLUMN Name | Data TYPE | Constraints | Description | | ------------- | ------------ | ----------------------------------- | ------------------------------ |
|
||||||
| id | INT | PRIMARY KEY,
|
| id | INT | PRIMARY KEY,
|
||||||
AUTO_INCREMENT | UNIQUE identifier FOR contract | | project_id | INT | NOT NULL,
|
AUTO_INCREMENT | UNIQUE identifier FOR contract | | uuid | UUID | NOT NULL, UNIQUE, DEFAULT UUID() | UUID Public Identifier (ADR-019) | | project_id | INT | NOT NULL,
|
||||||
FK | Reference TO projects TABLE | | contract_code | VARCHAR(50) | NOT NULL,
|
FK | Reference TO projects TABLE | | contract_code | VARCHAR(50) | NOT NULL,
|
||||||
UNIQUE | Contract code | | contract_name | VARCHAR(255) | NOT NULL | FULL contract name | | description | TEXT | NULL | Contract description | | start_date | DATE | NULL | Contract START date | | end_date | DATE | NULL | Contract
|
UNIQUE | Contract code | | contract_name | VARCHAR(255) | NOT NULL | FULL contract name | | description | TEXT | NULL | Contract description | | start_date | DATE | NULL | Contract START date | | end_date | DATE | NULL | Contract
|
||||||
END date | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last
|
END date | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last
|
||||||
@@ -152,7 +152,7 @@ UPDATE timestamp |
|
|||||||
|
|
||||||
* * Purpose **: MASTER TABLE storing ALL system users | COLUMN Name | Data TYPE | Constraints | Description | | ----------------------- | ------------ | ----------------------------------- | -------------------------------- |
|
* * Purpose **: MASTER TABLE storing ALL system users | COLUMN Name | Data TYPE | Constraints | Description | | ----------------------- | ------------ | ----------------------------------- | -------------------------------- |
|
||||||
| user_id | INT | PRIMARY KEY,
|
| user_id | INT | PRIMARY KEY,
|
||||||
AUTO_INCREMENT | UNIQUE identifier FOR user | | username | VARCHAR(50) | NOT NULL,
|
AUTO_INCREMENT | UNIQUE identifier FOR user | | uuid | UUID | NOT NULL, UNIQUE, DEFAULT UUID() | UUID Public Identifier (ADR-019) | | username | VARCHAR(50) | NOT NULL,
|
||||||
UNIQUE | Login username | | password_hash | VARCHAR(255) | NOT NULL | Hashed PASSWORD (bcrypt) | | first_name | VARCHAR(50) | NULL | User 's first name |
|
UNIQUE | Login username | | password_hash | VARCHAR(255) | NOT NULL | Hashed PASSWORD (bcrypt) | | first_name | VARCHAR(50) | NULL | User 's first name |
|
||||||
| last_name | VARCHAR(50) | NULL | User' s last name | | email | VARCHAR(100) | NOT NULL,
|
| last_name | VARCHAR(50) | NULL | User' s last name | | email | VARCHAR(100) | NOT NULL,
|
||||||
UNIQUE | Email address | | line_id | VARCHAR(100) | NULL | LINE messenger ID | | primary_organization_id | INT | NULL,
|
UNIQUE | Email address | | line_id | VARCHAR(100) | NULL | LINE messenger ID | | primary_organization_id | INT | NULL,
|
||||||
@@ -335,6 +335,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| ------------------------- | ------------ | --------------------------- | ------------------------------------------ |
|
| ------------------------- | ------------ | --------------------------- | ------------------------------------------ |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Master correspondence ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Master correspondence ID |
|
||||||
|
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
|
||||||
| correspondence_number | VARCHAR(100) | NOT NULL | Document number (from numbering system) |
|
| correspondence_number | VARCHAR(100) | NOT NULL | Document number (from numbering system) |
|
||||||
| correspondence_type_id | INT | NOT NULL, FK | Reference to correspondence_types |
|
| correspondence_type_id | INT | NOT NULL, FK | Reference to correspondence_types |
|
||||||
| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** |
|
| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** |
|
||||||
@@ -354,6 +355,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
* FOREIGN KEY (originator_id) REFERENCES organizations(id) ON DELETE SET NULL
|
* FOREIGN KEY (originator_id) REFERENCES organizations(id) ON DELETE SET NULL
|
||||||
* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL
|
* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL
|
||||||
* UNIQUE KEY (project_id, correspondence_number)
|
* UNIQUE KEY (project_id, correspondence_number)
|
||||||
|
* UNIQUE INDEX idx_correspondences_uuid (uuid)
|
||||||
* INDEX (correspondence_type_id)
|
* INDEX (correspondence_type_id)
|
||||||
* INDEX (originator_id)
|
* INDEX (originator_id)
|
||||||
* INDEX (deleted_at)
|
* INDEX (deleted_at)
|
||||||
@@ -372,6 +374,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| ------------------------ | ------------ | --------------------------------- | -------------------------------------------------------- |
|
| ------------------------ | ------------ | --------------------------------- | -------------------------------------------------------- |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
|
||||||
|
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
|
||||||
| correspondence_id | INT | NOT NULL, FK | Master correspondence ID |
|
| correspondence_id | INT | NOT NULL, FK | Master correspondence ID |
|
||||||
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
|
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
|
||||||
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) |
|
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) |
|
||||||
@@ -824,6 +827,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| --------------- | ------------ | ----------------------------------- | ---------------------------------------- |
|
| --------------- | ------------ | ----------------------------------- | ---------------------------------------- |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID |
|
||||||
|
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
|
||||||
| project_id | INT | NOT NULL, FK | Reference to projects |
|
| project_id | INT | NOT NULL, FK | Reference to projects |
|
||||||
| condwg_no | VARCHAR(255) | NOT NULL | Contract drawing number |
|
| condwg_no | VARCHAR(255) | NOT NULL | Contract drawing number |
|
||||||
| title | VARCHAR(255) | NOT NULL | Drawing title |
|
| title | VARCHAR(255) | NOT NULL | Drawing title |
|
||||||
@@ -843,6 +847,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
* FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes(id) ON DELETE RESTRICT
|
* FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes(id) ON DELETE RESTRICT
|
||||||
* FOREIGN KEY (updated_by) REFERENCES users(user_id)
|
* FOREIGN KEY (updated_by) REFERENCES users(user_id)
|
||||||
* UNIQUE KEY (project_id, condwg_no)
|
* UNIQUE KEY (project_id, condwg_no)
|
||||||
|
* UNIQUE INDEX idx_contract_drawings_uuid (uuid)
|
||||||
* INDEX (map_cat_id)
|
* INDEX (map_cat_id)
|
||||||
* INDEX (volume_id)
|
* INDEX (volume_id)
|
||||||
* INDEX (deleted_at)
|
* INDEX (deleted_at)
|
||||||
@@ -942,6 +947,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| ---------------- | ------------ | ----------------------------------- | -------------------------- |
|
| ---------------- | ------------ | ----------------------------------- | -------------------------- |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID |
|
||||||
|
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
|
||||||
| project_id | INT | NOT NULL, FK | Reference to projects |
|
| project_id | INT | NOT NULL, FK | Reference to projects |
|
||||||
| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | Shop drawing number |
|
| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | Shop drawing number |
|
||||||
| main_category_id | INT | NOT NULL, FK | Reference to main category |
|
| main_category_id | INT | NOT NULL, FK | Reference to main category |
|
||||||
@@ -955,6 +961,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
|
|
||||||
* PRIMARY KEY (id)
|
* PRIMARY KEY (id)
|
||||||
* UNIQUE (drawing_number)
|
* UNIQUE (drawing_number)
|
||||||
|
* UNIQUE INDEX idx_shop_drawings_uuid (uuid)
|
||||||
* FOREIGN KEY (project_id) REFERENCES projects(id)
|
* FOREIGN KEY (project_id) REFERENCES projects(id)
|
||||||
* FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id)
|
* FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id)
|
||||||
* FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id)
|
* FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id)
|
||||||
@@ -986,6 +993,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| ------------------------- | ---------------- | --------------------------- | ---------------------------------------- |
|
| ------------------------- | ---------------- | --------------------------- | ---------------------------------------- |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
|
||||||
|
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
|
||||||
| shop_drawing_id | INT | NOT NULL, FK | Master shop drawing ID |
|
| shop_drawing_id | INT | NOT NULL, FK | Master shop drawing ID |
|
||||||
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
|
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
|
||||||
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) |
|
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) |
|
||||||
@@ -1000,6 +1008,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
* PRIMARY KEY (id)
|
* PRIMARY KEY (id)
|
||||||
* FOREIGN KEY (shop_drawing_id) REFERENCES shop_drawings(id) ON DELETE CASCADE
|
* FOREIGN KEY (shop_drawing_id) REFERENCES shop_drawings(id) ON DELETE CASCADE
|
||||||
* UNIQUE KEY (shop_drawing_id, revision_number)
|
* UNIQUE KEY (shop_drawing_id, revision_number)
|
||||||
|
* UNIQUE INDEX idx_shop_drawing_revisions_uuid (uuid)
|
||||||
* INDEX (revision_date)
|
* INDEX (revision_date)
|
||||||
|
|
||||||
**Relationships**:
|
**Relationships**:
|
||||||
@@ -1053,6 +1062,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| ---------------- | ------------ | ----------------------------------- | -------------------------- |
|
| ---------------- | ------------ | ----------------------------------- | -------------------------- |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID |
|
||||||
|
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
|
||||||
| project_id | INT | NOT NULL, FK | Reference to projects |
|
| project_id | INT | NOT NULL, FK | Reference to projects |
|
||||||
| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | AS Built drawing number |
|
| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | AS Built drawing number |
|
||||||
| main_category_id | INT | NOT NULL, FK | Reference to main category |
|
| main_category_id | INT | NOT NULL, FK | Reference to main category |
|
||||||
@@ -1066,6 +1076,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
|
|
||||||
* PRIMARY KEY (id)
|
* PRIMARY KEY (id)
|
||||||
* UNIQUE (drawing_number)
|
* UNIQUE (drawing_number)
|
||||||
|
* UNIQUE INDEX idx_asbuilt_drawings_uuid (uuid)
|
||||||
* FOREIGN KEY (project_id) REFERENCES projects(id)
|
* FOREIGN KEY (project_id) REFERENCES projects(id)
|
||||||
* FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id)
|
* FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id)
|
||||||
* FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id)
|
* FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id)
|
||||||
@@ -1097,6 +1108,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| --------------------- | ------------ | --------------------------- | ------------------------------ |
|
| --------------------- | ------------ | --------------------------- | ------------------------------ |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
|
||||||
|
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
|
||||||
| asbuilt_drawing_id | INT | NOT NULL, FK | Master AS Built drawing ID |
|
| asbuilt_drawing_id | INT | NOT NULL, FK | Master AS Built drawing ID |
|
||||||
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
|
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
|
||||||
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) |
|
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) |
|
||||||
@@ -1111,6 +1123,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
* PRIMARY KEY (id)
|
* PRIMARY KEY (id)
|
||||||
* FOREIGN KEY (asbuilt_drawing_id) REFERENCES asbuilt_drawings(id) ON DELETE CASCADE
|
* FOREIGN KEY (asbuilt_drawing_id) REFERENCES asbuilt_drawings(id) ON DELETE CASCADE
|
||||||
* UNIQUE KEY (asbuilt_drawing_id, revision_number)
|
* UNIQUE KEY (asbuilt_drawing_id, revision_number)
|
||||||
|
* UNIQUE INDEX idx_asbuilt_drawing_revisions_uuid (uuid)
|
||||||
* INDEX (revision_date)
|
* INDEX (revision_date)
|
||||||
|
|
||||||
**Relationships**:
|
**Relationships**:
|
||||||
@@ -1229,6 +1242,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| ----------------------- | ------------ | ----------------------------------- | ----------------------------------------- |
|
| ----------------------- | ------------ | ----------------------------------- | ----------------------------------------- |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique circulation ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique circulation ID |
|
||||||
|
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
|
||||||
| correspondence_id | INT | UNIQUE, FK | Link to correspondence (1:1 relationship) |
|
| correspondence_id | INT | UNIQUE, FK | Link to correspondence (1:1 relationship) |
|
||||||
| organization_id | INT | NOT NULL, FK | Organization that owns this circulation |
|
| organization_id | INT | NOT NULL, FK | Organization that owns this circulation |
|
||||||
| circulation_no | VARCHAR(100) | NOT NULL | Circulation sheet number |
|
| circulation_no | VARCHAR(100) | NOT NULL | Circulation sheet number |
|
||||||
@@ -1249,6 +1263,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
* FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes(code)
|
* FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes(code)
|
||||||
* FOREIGN KEY (created_by_user_id) REFERENCES users(user_id)
|
* FOREIGN KEY (created_by_user_id) REFERENCES users(user_id)
|
||||||
* INDEX (organization_id)
|
* INDEX (organization_id)
|
||||||
|
* UNIQUE INDEX idx_circulations_uuid (uuid)
|
||||||
* INDEX (circulation_status_code)
|
* INDEX (circulation_status_code)
|
||||||
* INDEX (created_by_user_id)
|
* INDEX (created_by_user_id)
|
||||||
|
|
||||||
@@ -1338,6 +1353,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| ------------------- | ------------ | --------------------------- | ------------------------------------------------------------------------ |
|
| ------------------- | ------------ | --------------------------- | ------------------------------------------------------------------------ |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique attachment ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique attachment ID |
|
||||||
|
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
|
||||||
| original_filename | VARCHAR(255) | NOT NULL | Original filename from upload |
|
| original_filename | VARCHAR(255) | NOT NULL | Original filename from upload |
|
||||||
| stored_filename | VARCHAR(255) | NOT NULL | System-generated unique filename |
|
| stored_filename | VARCHAR(255) | NOT NULL | System-generated unique filename |
|
||||||
| file_path | VARCHAR(500) | NOT NULL | Full file path on server (/share/dms-data/) |
|
| file_path | VARCHAR(500) | NOT NULL | Full file path on server (/share/dms-data/) |
|
||||||
@@ -1358,6 +1374,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
* INDEX (stored_filename)
|
* INDEX (stored_filename)
|
||||||
* INDEX (mime_type)
|
* INDEX (mime_type)
|
||||||
* INDEX (uploaded_by_user_id)
|
* INDEX (uploaded_by_user_id)
|
||||||
|
* UNIQUE INDEX idx_attachments_uuid (uuid)
|
||||||
* INDEX (created_at)
|
* INDEX (created_at)
|
||||||
* INDEX (reference_date)
|
* INDEX (reference_date)
|
||||||
|
|
||||||
@@ -1820,6 +1837,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
| Column Name | Data Type | Constraints | Description |
|
| Column Name | Data Type | Constraints | Description |
|
||||||
| :---------------- | :----------- | :-------------------------- | :------------------------ |
|
| :---------------- | :----------- | :-------------------------- | :------------------------ |
|
||||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique notification ID |
|
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique notification ID |
|
||||||
|
| uuid | UUID | NOT NULL, DEFAULT | UUID Public Identifier (ADR-019) |
|
||||||
| user_id | INT | NOT NULL, FK | Recipient user ID |
|
| user_id | INT | NOT NULL, FK | Recipient user ID |
|
||||||
| title | VARCHAR(255) | NOT NULL | Notification title |
|
| title | VARCHAR(255) | NOT NULL | Notification title |
|
||||||
| message | TEXT | NOT NULL | Notification body |
|
| message | TEXT | NOT NULL | Notification body |
|
||||||
@@ -1836,6 +1854,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
|
|||||||
* INDEX idx_notif_type (notification_type)
|
* INDEX idx_notif_type (notification_type)
|
||||||
* INDEX idx_notif_read (is_read)
|
* INDEX idx_notif_read (is_read)
|
||||||
* INDEX idx_notif_created (created_at)
|
* INDEX idx_notif_created (created_at)
|
||||||
|
* INDEX idx_notifications_uuid (uuid)
|
||||||
|
|
||||||
**Partitioning**:
|
**Partitioning**:
|
||||||
* **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี
|
* **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ CREATE TABLE organization_roles (
|
|||||||
-- ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ
|
-- ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ
|
||||||
CREATE TABLE organizations (
|
CREATE TABLE organizations (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
organization_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสองค์กร',
|
organization_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสองค์กร',
|
||||||
organization_name VARCHAR(255) NOT NULL COMMENT 'ชื่อองค์กร',
|
organization_name VARCHAR(255) NOT NULL COMMENT 'ชื่อองค์กร',
|
||||||
role_id INT COMMENT 'บทบาทขององค์กร',
|
role_id INT COMMENT 'บทบาทขององค์กร',
|
||||||
@@ -31,12 +32,14 @@ CREATE TABLE organizations (
|
|||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||||
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)',
|
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)',
|
||||||
FOREIGN KEY (role_id) REFERENCES organization_roles (id) ON DELETE
|
FOREIGN KEY (role_id) REFERENCES organization_roles (id) ON DELETE
|
||||||
SET NULL
|
SET NULL,
|
||||||
|
UNIQUE INDEX idx_organizations_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ';
|
||||||
|
|
||||||
-- ตาราง Master เก็บข้อมูลโครงการ
|
-- ตาราง Master เก็บข้อมูลโครงการ
|
||||||
CREATE TABLE projects (
|
CREATE TABLE projects (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
project_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสโครงการ',
|
project_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสโครงการ',
|
||||||
project_name VARCHAR(255) NOT NULL COMMENT 'ชื่อโครงการ',
|
project_name VARCHAR(255) NOT NULL COMMENT 'ชื่อโครงการ',
|
||||||
-- parent_project_id INT COMMENT 'รหัสโครงการหลัก (ถ้ามี)',
|
-- parent_project_id INT COMMENT 'รหัสโครงการหลัก (ถ้ามี)',
|
||||||
@@ -46,12 +49,14 @@ CREATE TABLE projects (
|
|||||||
-- FOREIGN KEY (contractor_organization_id) REFERENCES organizations(id) ON DELETE SET NULL
|
-- FOREIGN KEY (contractor_organization_id) REFERENCES organizations(id) ON DELETE SET NULL
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||||
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)'
|
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)',
|
||||||
|
UNIQUE INDEX idx_projects_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลโครงการ';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลโครงการ';
|
||||||
|
|
||||||
-- ตาราง Master เก็บข้อมูลสัญญา
|
-- ตาราง Master เก็บข้อมูลสัญญา
|
||||||
CREATE TABLE contracts (
|
CREATE TABLE contracts (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
project_id INT NOT NULL,
|
project_id INT NOT NULL,
|
||||||
contract_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสัญญา',
|
contract_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสัญญา',
|
||||||
contract_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสัญญา',
|
contract_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสัญญา',
|
||||||
@@ -62,7 +67,8 @@ CREATE TABLE contracts (
|
|||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||||
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)',
|
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)',
|
||||||
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
|
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
|
||||||
|
UNIQUE INDEX idx_contracts_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลสัญญา';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลสัญญา';
|
||||||
|
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
@@ -71,6 +77,7 @@ CREATE TABLE contracts (
|
|||||||
-- ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)
|
-- ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
username VARCHAR(50) NOT NULL UNIQUE COMMENT 'ชื่อผู้ใช้งาน',
|
username VARCHAR(50) NOT NULL UNIQUE COMMENT 'ชื่อผู้ใช้งาน',
|
||||||
password_hash VARCHAR(255) NOT NULL COMMENT 'รหัสผ่าน (Hashed)',
|
password_hash VARCHAR(255) NOT NULL COMMENT 'รหัสผ่าน (Hashed)',
|
||||||
first_name VARCHAR(50) COMMENT 'ชื่อจริง',
|
first_name VARCHAR(50) COMMENT 'ชื่อจริง',
|
||||||
@@ -86,7 +93,8 @@ CREATE TABLE users (
|
|||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
|
||||||
deleted_at DATETIME NULL DEFAULT NULL COMMENT 'วันที่ลบ',
|
deleted_at DATETIME NULL DEFAULT NULL COMMENT 'วันที่ลบ',
|
||||||
FOREIGN KEY (primary_organization_id) REFERENCES organizations (id) ON DELETE
|
FOREIGN KEY (primary_organization_id) REFERENCES organizations (id) ON DELETE
|
||||||
SET NULL
|
SET NULL,
|
||||||
|
UNIQUE INDEX idx_users_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)';
|
||||||
|
|
||||||
-- ตารางเก็บ Refresh Tokens สำหรับ Authentication
|
-- ตารางเก็บ Refresh Tokens สำหรับ Authentication
|
||||||
@@ -258,6 +266,7 @@ CREATE TABLE correspondence_status (
|
|||||||
-- ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision
|
-- ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision
|
||||||
CREATE TABLE correspondences (
|
CREATE TABLE correspondences (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง (นี่คือ "Master ID" ที่ใช้เชื่อมโยง)',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง (นี่คือ "Master ID" ที่ใช้เชื่อมโยง)',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร ใช้แบ่งแยกว่าเป็น RFA หรือ อื่นๆ',
|
correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร ใช้แบ่งแยกว่าเป็น RFA หรือ อื่นๆ',
|
||||||
correspondence_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสาร (สร้างจาก DocumentNumberingModule)',
|
correspondence_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสาร (สร้างจาก DocumentNumberingModule)',
|
||||||
discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)',
|
discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)',
|
||||||
@@ -279,7 +288,8 @@ CREATE TABLE correspondences (
|
|||||||
-- Foreign Key ที่รวมเข้ามาจาก ALTER (ระบุชื่อ Constraint ตามที่ต้องการ)
|
-- Foreign Key ที่รวมเข้ามาจาก ALTER (ระบุชื่อ Constraint ตามที่ต้องการ)
|
||||||
CONSTRAINT fk_corr_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE
|
CONSTRAINT fk_corr_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE
|
||||||
SET NULL,
|
SET NULL,
|
||||||
UNIQUE KEY uq_corr_no_per_project (project_id, correspondence_number)
|
UNIQUE KEY uq_corr_no_per_project (project_id, correspondence_number),
|
||||||
|
UNIQUE INDEX idx_correspondences_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision';
|
||||||
|
|
||||||
-- ตารางเชื่อมผู้รับ (TO/CC) สำหรับเอกสารแต่ละฉบับ (M:N)
|
-- ตารางเชื่อมผู้รับ (TO/CC) สำหรับเอกสารแต่ละฉบับ (M:N)
|
||||||
@@ -299,6 +309,7 @@ CREATE TABLE correspondence_recipients (
|
|||||||
-- ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1:N)
|
-- ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1:N)
|
||||||
CREATE TABLE correspondence_revisions (
|
CREATE TABLE correspondence_revisions (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
correspondence_id INT NOT NULL COMMENT 'Master ID',
|
correspondence_id INT NOT NULL COMMENT 'Master ID',
|
||||||
revision_number INT NOT NULL COMMENT 'หมายเลข Revision (0, 1, 2...)',
|
revision_number INT NOT NULL COMMENT 'หมายเลข Revision (0, 1, 2...)',
|
||||||
revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)',
|
revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)',
|
||||||
@@ -338,7 +349,8 @@ CREATE TABLE correspondence_revisions (
|
|||||||
UNIQUE KEY uq_master_revision_number (correspondence_id, revision_number),
|
UNIQUE KEY uq_master_revision_number (correspondence_id, revision_number),
|
||||||
UNIQUE KEY uq_master_current (correspondence_id, is_current),
|
UNIQUE KEY uq_master_current (correspondence_id, is_current),
|
||||||
INDEX idx_corr_rev_v_project (v_ref_project_id),
|
INDEX idx_corr_rev_v_project (v_ref_project_id),
|
||||||
INDEX idx_corr_rev_v_subtype (v_doc_subtype)
|
INDEX idx_corr_rev_v_subtype (v_doc_subtype),
|
||||||
|
UNIQUE INDEX idx_correspondence_revisions_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1 :N)';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1 :N)';
|
||||||
|
|
||||||
-- ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ
|
-- ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ
|
||||||
@@ -529,6 +541,7 @@ CREATE TABLE contract_drawing_subcat_cat_maps (
|
|||||||
-- ตาราง Master เก็บข้อมูล "แบบคู่สัญญา"
|
-- ตาราง Master เก็บข้อมูล "แบบคู่สัญญา"
|
||||||
CREATE TABLE contract_drawings (
|
CREATE TABLE contract_drawings (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
project_id INT NOT NULL COMMENT 'โครงการ',
|
project_id INT NOT NULL COMMENT 'โครงการ',
|
||||||
condwg_no VARCHAR(255) NOT NULL COMMENT 'เลขที่แบบสัญญา',
|
condwg_no VARCHAR(255) NOT NULL COMMENT 'เลขที่แบบสัญญา',
|
||||||
title VARCHAR(255) NOT NULL COMMENT 'ชื่อแบบสัญญา',
|
title VARCHAR(255) NOT NULL COMMENT 'ชื่อแบบสัญญา',
|
||||||
@@ -542,7 +555,8 @@ CREATE TABLE contract_drawings (
|
|||||||
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
|
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (map_cat_id) REFERENCES contract_drawing_subcat_cat_maps (id) ON DELETE RESTRICT,
|
FOREIGN KEY (map_cat_id) REFERENCES contract_drawing_subcat_cat_maps (id) ON DELETE RESTRICT,
|
||||||
FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes (id) ON DELETE RESTRICT,
|
FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes (id) ON DELETE RESTRICT,
|
||||||
UNIQUE KEY ux_condwg_no_project (project_id, condwg_no)
|
UNIQUE KEY ux_condwg_no_project (project_id, condwg_no),
|
||||||
|
UNIQUE INDEX idx_contract_drawings_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบคู่สัญญา"';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบคู่สัญญา"';
|
||||||
|
|
||||||
-- ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบก่อสร้าง
|
-- ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบก่อสร้าง
|
||||||
@@ -576,6 +590,7 @@ CREATE TABLE shop_drawing_sub_categories (
|
|||||||
-- ตาราง Master เก็บข้อมูล "แบบก่อสร้าง"
|
-- ตาราง Master เก็บข้อมูล "แบบก่อสร้าง"
|
||||||
CREATE TABLE shop_drawings (
|
CREATE TABLE shop_drawings (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
project_id INT NOT NULL COMMENT 'โครงการ',
|
project_id INT NOT NULL COMMENT 'โครงการ',
|
||||||
drawing_number VARCHAR(100) NOT NULL COMMENT 'เลขที่ Shop Drawing',
|
drawing_number VARCHAR(100) NOT NULL COMMENT 'เลขที่ Shop Drawing',
|
||||||
main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก',
|
main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก',
|
||||||
@@ -587,12 +602,14 @@ CREATE TABLE shop_drawings (
|
|||||||
FOREIGN KEY (project_id) REFERENCES projects (id),
|
FOREIGN KEY (project_id) REFERENCES projects (id),
|
||||||
FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id),
|
FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id),
|
||||||
FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories (id),
|
FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories (id),
|
||||||
UNIQUE KEY ux_shop_dwg_no_project (project_id, drawing_number)
|
UNIQUE KEY ux_shop_dwg_no_project (project_id, drawing_number),
|
||||||
|
UNIQUE INDEX idx_shop_drawings_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบก่อสร้าง"';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบก่อสร้าง"';
|
||||||
|
|
||||||
-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1:N)
|
-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1:N)
|
||||||
CREATE TABLE shop_drawing_revisions (
|
CREATE TABLE shop_drawing_revisions (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
shop_drawing_id INT NOT NULL COMMENT 'Master ID',
|
shop_drawing_id INT NOT NULL COMMENT 'Master ID',
|
||||||
revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)',
|
revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)',
|
||||||
revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)',
|
revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)',
|
||||||
@@ -610,7 +627,8 @@ CREATE TABLE shop_drawing_revisions (
|
|||||||
FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE
|
FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE
|
||||||
SET NULL,
|
SET NULL,
|
||||||
UNIQUE KEY ux_sd_rev_drawing_revision (shop_drawing_id, revision_number),
|
UNIQUE KEY ux_sd_rev_drawing_revision (shop_drawing_id, revision_number),
|
||||||
UNIQUE KEY uq_sd_current (shop_drawing_id, is_current)
|
UNIQUE KEY uq_sd_current (shop_drawing_id, is_current),
|
||||||
|
UNIQUE INDEX idx_shop_drawing_revisions_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1:N)';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1:N)';
|
||||||
|
|
||||||
-- ตารางเชื่อมระหว่าง shop_drawing_revisions กับ contract_drawings (M:N)
|
-- ตารางเชื่อมระหว่าง shop_drawing_revisions กับ contract_drawings (M:N)
|
||||||
@@ -628,6 +646,7 @@ CREATE TABLE shop_drawing_revision_contract_refs (
|
|||||||
-- ตาราง Master เก็บข้อมูล "AS Built"
|
-- ตาราง Master เก็บข้อมูล "AS Built"
|
||||||
CREATE TABLE asbuilt_drawings (
|
CREATE TABLE asbuilt_drawings (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
project_id INT NOT NULL COMMENT 'โครงการ',
|
project_id INT NOT NULL COMMENT 'โครงการ',
|
||||||
drawing_number VARCHAR(100) NOT NULL COMMENT 'เลขที่ AS Built Drawing',
|
drawing_number VARCHAR(100) NOT NULL COMMENT 'เลขที่ AS Built Drawing',
|
||||||
main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก',
|
main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก',
|
||||||
@@ -639,12 +658,14 @@ CREATE TABLE asbuilt_drawings (
|
|||||||
FOREIGN KEY (project_id) REFERENCES projects (id),
|
FOREIGN KEY (project_id) REFERENCES projects (id),
|
||||||
FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id),
|
FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id),
|
||||||
FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories (id),
|
FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories (id),
|
||||||
UNIQUE KEY ux_asbuilt_no_project (project_id, drawing_number)
|
UNIQUE KEY ux_asbuilt_no_project (project_id, drawing_number),
|
||||||
|
UNIQUE INDEX idx_asbuilt_drawings_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบ AS Built"';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบ AS Built"';
|
||||||
|
|
||||||
-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ AS Built (1:N)
|
-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ AS Built (1:N)
|
||||||
CREATE TABLE asbuilt_drawing_revisions (
|
CREATE TABLE asbuilt_drawing_revisions (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
asbuilt_drawing_id INT NOT NULL COMMENT 'Master ID',
|
asbuilt_drawing_id INT NOT NULL COMMENT 'Master ID',
|
||||||
revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)',
|
revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)',
|
||||||
revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)',
|
revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)',
|
||||||
@@ -662,7 +683,8 @@ CREATE TABLE asbuilt_drawing_revisions (
|
|||||||
FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE
|
FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE
|
||||||
SET NULL,
|
SET NULL,
|
||||||
UNIQUE KEY ux_asbuilt_rev_drawing_revision (asbuilt_drawing_id, revision_number),
|
UNIQUE KEY ux_asbuilt_rev_drawing_revision (asbuilt_drawing_id, revision_number),
|
||||||
UNIQUE KEY uq_asbuilt_current (asbuilt_drawing_id, is_current)
|
UNIQUE KEY uq_asbuilt_current (asbuilt_drawing_id, is_current),
|
||||||
|
UNIQUE INDEX idx_asbuilt_drawing_revisions_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ asbuilt_drawings (1:N)';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ asbuilt_drawings (1:N)';
|
||||||
|
|
||||||
-- ตารางเชื่อมระหว่าง asbuilt_drawing_revisions กับ shop_drawing_revisions (M:N)
|
-- ตารางเชื่อมระหว่าง asbuilt_drawing_revisions กับ shop_drawing_revisions (M:N)
|
||||||
@@ -744,6 +766,7 @@ CREATE TABLE circulation_status_codes (
|
|||||||
-- ตาราง "แม่" ของใบเวียนเอกสารภายใน
|
-- ตาราง "แม่" ของใบเวียนเอกสารภายใน
|
||||||
CREATE TABLE circulations (
|
CREATE TABLE circulations (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตารางใบเวียน',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตารางใบเวียน',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
correspondence_id INT UNIQUE COMMENT 'ID ของเอกสาร (จากตาราง correspondences)',
|
correspondence_id INT UNIQUE COMMENT 'ID ของเอกสาร (จากตาราง correspondences)',
|
||||||
organization_id INT NOT NULL COMMENT 'ID ขององค์กรณ์ที่เป็นเจ้าของใบเวียนนี้',
|
organization_id INT NOT NULL COMMENT 'ID ขององค์กรณ์ที่เป็นเจ้าของใบเวียนนี้',
|
||||||
circulation_no VARCHAR(100) NOT NULL COMMENT 'เลขที่ใบเวียน',
|
circulation_no VARCHAR(100) NOT NULL COMMENT 'เลขที่ใบเวียน',
|
||||||
@@ -757,7 +780,8 @@ CREATE TABLE circulations (
|
|||||||
FOREIGN KEY (correspondence_id) REFERENCES correspondences (id),
|
FOREIGN KEY (correspondence_id) REFERENCES correspondences (id),
|
||||||
FOREIGN KEY (organization_id) REFERENCES organizations (id),
|
FOREIGN KEY (organization_id) REFERENCES organizations (id),
|
||||||
FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes (code),
|
FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes (code),
|
||||||
FOREIGN KEY (created_by_user_id) REFERENCES users (user_id)
|
FOREIGN KEY (created_by_user_id) REFERENCES users (user_id),
|
||||||
|
UNIQUE INDEX idx_circulations_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของใบเวียนเอกสารภายใน';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของใบเวียนเอกสารภายใน';
|
||||||
|
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
@@ -800,6 +824,7 @@ CREATE TABLE transmittal_items (
|
|||||||
-- เหตุผล: จัดการไฟล์ขยะ (Orphan Files) และตรวจสอบความถูกต้องไฟล์
|
-- เหตุผล: จัดการไฟล์ขยะ (Orphan Files) และตรวจสอบความถูกต้องไฟล์
|
||||||
CREATE TABLE attachments (
|
CREATE TABLE attachments (
|
||||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของไฟล์แนบ',
|
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของไฟล์แนบ',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
original_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ดั้งเดิมตอนอัปโหลด',
|
original_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ดั้งเดิมตอนอัปโหลด',
|
||||||
stored_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ที่เก็บจริงบน Server (ป้องกันชื่อซ้ำ)',
|
stored_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ที่เก็บจริงบน Server (ป้องกันชื่อซ้ำ)',
|
||||||
file_path VARCHAR(500) NOT NULL COMMENT 'Path ที่เก็บไฟล์ (บน QNAP / share / dms - data /)',
|
file_path VARCHAR(500) NOT NULL COMMENT 'Path ที่เก็บไฟล์ (บน QNAP / share / dms - data /)',
|
||||||
@@ -813,7 +838,8 @@ CREATE TABLE attachments (
|
|||||||
CHECKSUM VARCHAR(64) NULL COMMENT 'SHA-256 Checksum',
|
CHECKSUM VARCHAR(64) NULL COMMENT 'SHA-256 Checksum',
|
||||||
reference_date DATE NULL COMMENT 'Date used for folder structure (e.g. Issue Date) to prevent broken paths',
|
reference_date DATE NULL COMMENT 'Date used for folder structure (e.g. Issue Date) to prevent broken paths',
|
||||||
FOREIGN KEY (uploaded_by_user_id) REFERENCES users (user_id) ON DELETE CASCADE,
|
FOREIGN KEY (uploaded_by_user_id) REFERENCES users (user_id) ON DELETE CASCADE,
|
||||||
INDEX idx_attachments_reference_date (reference_date)
|
INDEX idx_attachments_reference_date (reference_date),
|
||||||
|
UNIQUE INDEX idx_attachments_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "กลาง" เก็บไฟล์แนบทั้งหมดของระบบ';
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "กลาง" เก็บไฟล์แนบทั้งหมดของระบบ';
|
||||||
|
|
||||||
-- ตารางเชื่อม correspondences กับ attachments (M:N)
|
-- ตารางเชื่อม correspondences กับ attachments (M:N)
|
||||||
@@ -1198,6 +1224,7 @@ PARTITION BY RANGE (YEAR(created_at)) (
|
|||||||
-- ตารางสำหรับจัดการการแจ้งเตือน (Email/Line/System)
|
-- ตารางสำหรับจัดการการแจ้งเตือน (Email/Line/System)
|
||||||
CREATE TABLE notifications (
|
CREATE TABLE notifications (
|
||||||
id INT NOT NULL AUTO_INCREMENT COMMENT 'ID ของการแจ้งเตือน',
|
id INT NOT NULL AUTO_INCREMENT COMMENT 'ID ของการแจ้งเตือน',
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
user_id INT NOT NULL COMMENT 'ID ผู้ใช้',
|
user_id INT NOT NULL COMMENT 'ID ผู้ใช้',
|
||||||
title VARCHAR(255) NOT NULL COMMENT 'หัวข้อการแจ้งเตือน',
|
title VARCHAR(255) NOT NULL COMMENT 'หัวข้อการแจ้งเตือน',
|
||||||
message TEXT NOT NULL COMMENT 'รายละเอียดการแจ้งเตือน',
|
message TEXT NOT NULL COMMENT 'รายละเอียดการแจ้งเตือน',
|
||||||
@@ -1213,7 +1240,8 @@ CREATE TABLE notifications (
|
|||||||
INDEX idx_notif_user (user_id),
|
INDEX idx_notif_user (user_id),
|
||||||
INDEX idx_notif_type (notification_type),
|
INDEX idx_notif_type (notification_type),
|
||||||
INDEX idx_notif_read (is_read),
|
INDEX idx_notif_read (is_read),
|
||||||
INDEX idx_notif_created (created_at)
|
INDEX idx_notif_created (created_at),
|
||||||
|
INDEX idx_notifications_uuid (uuid)
|
||||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการการแจ้งเตือน (Email / Line / System)' -- [เพิ่ม] คำสั่ง Partition
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการการแจ้งเตือน (Email / Line / System)' -- [เพิ่ม] คำสั่ง Partition
|
||||||
PARTITION BY RANGE (YEAR(created_at)) (
|
PARTITION BY RANGE (YEAR(created_at)) (
|
||||||
PARTITION p_old
|
PARTITION p_old
|
||||||
|
|||||||
@@ -120,17 +120,21 @@ CREATE INDEX idx_audit_request_id ON audit_logs (request_id);
|
|||||||
-- View แสดง Revision "ปัจจุบัน" ของ correspondences ทั้งหมด (ที่ไม่ใช่ RFA)
|
-- View แสดง Revision "ปัจจุบัน" ของ correspondences ทั้งหมด (ที่ไม่ใช่ RFA)
|
||||||
CREATE VIEW v_current_correspondences AS
|
CREATE VIEW v_current_correspondences AS
|
||||||
SELECT c.id AS correspondence_id,
|
SELECT c.id AS correspondence_id,
|
||||||
|
c.uuid AS correspondence_uuid,
|
||||||
c.correspondence_number,
|
c.correspondence_number,
|
||||||
c.correspondence_type_id,
|
c.correspondence_type_id,
|
||||||
ct.type_code AS correspondence_type_code,
|
ct.type_code AS correspondence_type_code,
|
||||||
ct.type_name AS correspondence_type_name,
|
ct.type_name AS correspondence_type_name,
|
||||||
c.project_id,
|
c.project_id,
|
||||||
|
p.uuid AS project_uuid,
|
||||||
p.project_code,
|
p.project_code,
|
||||||
p.project_name,
|
p.project_name,
|
||||||
c.originator_id,
|
c.originator_id,
|
||||||
|
org.uuid AS originator_uuid,
|
||||||
org.organization_code AS originator_code,
|
org.organization_code AS originator_code,
|
||||||
org.organization_name AS originator_name,
|
org.organization_name AS originator_name,
|
||||||
cr.id AS revision_id,
|
cr.id AS revision_id,
|
||||||
|
cr.uuid AS revision_uuid,
|
||||||
cr.revision_number,
|
cr.revision_number,
|
||||||
cr.revision_label,
|
cr.revision_label,
|
||||||
cr.subject,
|
cr.subject,
|
||||||
@@ -162,6 +166,7 @@ WHERE cr.is_current = TRUE
|
|||||||
-- View แสดง Revision "ปัจจุบัน" ของ rfa_revisions ทั้งหมด
|
-- View แสดง Revision "ปัจจุบัน" ของ rfa_revisions ทั้งหมด
|
||||||
CREATE VIEW v_current_rfas AS
|
CREATE VIEW v_current_rfas AS
|
||||||
SELECT r.id AS rfa_id,
|
SELECT r.id AS rfa_id,
|
||||||
|
c.uuid AS correspondence_uuid,
|
||||||
r.rfa_type_id,
|
r.rfa_type_id,
|
||||||
rt.type_code AS rfa_type_code,
|
rt.type_code AS rfa_type_code,
|
||||||
rt.type_name_th AS rfa_type_name_th,
|
rt.type_name_th AS rfa_type_name_th,
|
||||||
@@ -172,11 +177,14 @@ SELECT r.id AS rfa_id,
|
|||||||
d.discipline_code,
|
d.discipline_code,
|
||||||
-- ✅ Join เพิ่มเพื่อแสดง code
|
-- ✅ Join เพิ่มเพื่อแสดง code
|
||||||
c.project_id,
|
c.project_id,
|
||||||
|
p.uuid AS project_uuid,
|
||||||
p.project_code,
|
p.project_code,
|
||||||
p.project_name,
|
p.project_name,
|
||||||
c.originator_id,
|
c.originator_id,
|
||||||
|
org.uuid AS originator_uuid,
|
||||||
org.organization_name AS originator_name,
|
org.organization_name AS originator_name,
|
||||||
rr.id AS revision_id,
|
rr.id AS revision_id,
|
||||||
|
cr.uuid AS revision_uuid,
|
||||||
cr.revision_number,
|
cr.revision_number,
|
||||||
cr.revision_label,
|
cr.revision_label,
|
||||||
cr.subject,
|
cr.subject,
|
||||||
@@ -211,12 +219,15 @@ WHERE cr.is_current = TRUE
|
|||||||
-- View แสดงความสัมพันธ์ทั้งหมดระหว่าง Contract, Project, และ Organization
|
-- View แสดงความสัมพันธ์ทั้งหมดระหว่าง Contract, Project, และ Organization
|
||||||
CREATE VIEW v_contract_parties_all AS
|
CREATE VIEW v_contract_parties_all AS
|
||||||
SELECT c.id AS contract_id,
|
SELECT c.id AS contract_id,
|
||||||
|
c.uuid AS contract_uuid,
|
||||||
c.contract_code,
|
c.contract_code,
|
||||||
c.contract_name,
|
c.contract_name,
|
||||||
p.id AS project_id,
|
p.id AS project_id,
|
||||||
|
p.uuid AS project_uuid,
|
||||||
p.project_code,
|
p.project_code,
|
||||||
p.project_name,
|
p.project_name,
|
||||||
o.id AS organization_id,
|
o.id AS organization_id,
|
||||||
|
o.uuid AS organization_uuid,
|
||||||
o.organization_code,
|
o.organization_code,
|
||||||
o.organization_name,
|
o.organization_name,
|
||||||
co.role_in_contract
|
co.role_in_contract
|
||||||
@@ -380,8 +391,10 @@ WHERE p.is_active = 1
|
|||||||
CREATE VIEW v_documents_with_attachments AS
|
CREATE VIEW v_documents_with_attachments AS
|
||||||
SELECT 'CORRESPONDENCE' AS document_type,
|
SELECT 'CORRESPONDENCE' AS document_type,
|
||||||
c.id AS document_id,
|
c.id AS document_id,
|
||||||
|
c.uuid AS document_uuid,
|
||||||
c.correspondence_number AS document_number,
|
c.correspondence_number AS document_number,
|
||||||
c.project_id,
|
c.project_id,
|
||||||
|
p.uuid AS project_uuid,
|
||||||
p.project_code,
|
p.project_code,
|
||||||
p.project_name,
|
p.project_name,
|
||||||
COUNT(ca.attachment_id) AS attachment_count,
|
COUNT(ca.attachment_id) AS attachment_count,
|
||||||
@@ -399,8 +412,10 @@ GROUP BY c.id,
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'CIRCULATION' AS document_type,
|
SELECT 'CIRCULATION' AS document_type,
|
||||||
circ.id AS document_id,
|
circ.id AS document_id,
|
||||||
|
circ.uuid AS document_uuid,
|
||||||
circ.circulation_no AS document_number,
|
circ.circulation_no AS document_number,
|
||||||
corr.project_id,
|
corr.project_id,
|
||||||
|
p.uuid AS project_uuid,
|
||||||
p.project_code,
|
p.project_code,
|
||||||
p.project_name,
|
p.project_name,
|
||||||
COUNT(ca.attachment_id) AS attachment_count,
|
COUNT(ca.attachment_id) AS attachment_count,
|
||||||
@@ -418,8 +433,10 @@ GROUP BY circ.id,
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SHOP_DRAWING' AS document_type,
|
SELECT 'SHOP_DRAWING' AS document_type,
|
||||||
sdr.id AS document_id,
|
sdr.id AS document_id,
|
||||||
|
sdr.uuid AS document_uuid,
|
||||||
sd.drawing_number AS document_number,
|
sd.drawing_number AS document_number,
|
||||||
sd.project_id,
|
sd.project_id,
|
||||||
|
p.uuid AS project_uuid,
|
||||||
p.project_code,
|
p.project_code,
|
||||||
p.project_name,
|
p.project_name,
|
||||||
COUNT(sdra.attachment_id) AS attachment_count,
|
COUNT(sdra.attachment_id) AS attachment_count,
|
||||||
@@ -438,8 +455,10 @@ GROUP BY sdr.id,
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'CONTRACT_DRAWING' AS document_type,
|
SELECT 'CONTRACT_DRAWING' AS document_type,
|
||||||
cd.id AS document_id,
|
cd.id AS document_id,
|
||||||
|
cd.uuid AS document_uuid,
|
||||||
cd.condwg_no AS document_number,
|
cd.condwg_no AS document_number,
|
||||||
cd.project_id,
|
cd.project_id,
|
||||||
|
p.uuid AS project_uuid,
|
||||||
p.project_code,
|
p.project_code,
|
||||||
p.project_name,
|
p.project_name,
|
||||||
COUNT(cda.attachment_id) AS attachment_count,
|
COUNT(cda.attachment_id) AS attachment_count,
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
# Implementation Plan: Hybrid UUID Strategy (ADR-019)
|
||||||
|
|
||||||
|
**Version:** 1.8.1
|
||||||
|
**Created:** 2026-03-16
|
||||||
|
**Related ADR:** [ADR-019](../06-Decision-Records/ADR-019-hybrid-identifier-strategy.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the step-by-step implementation plan to integrate UUIDv7 public identifiers into the LCBP3-DMS backend, following the hybrid strategy defined in ADR-019.
|
||||||
|
|
||||||
|
**Scope:** 14 public-facing tables now have `uuid UUID` columns (MariaDB native type, stored as BINARY(16) internally) in the schema. This plan covers backend code changes to expose UUIDs through the API while keeping INT PKs for internal operations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Database Foundation (✅ COMPLETED)
|
||||||
|
|
||||||
|
- [x] Create ADR-019 document
|
||||||
|
- [x] Add `uuid UUID` columns (MariaDB native type) to 14 public-facing tables in schema SQL
|
||||||
|
- [x] Add UNIQUE INDEX on each uuid column
|
||||||
|
- [x] Update data dictionary with uuid column documentation
|
||||||
|
- [x] Update AGENTS.md with ADR-019 reference
|
||||||
|
|
||||||
|
### Affected Tables (14)
|
||||||
|
|
||||||
|
| # | Table | PK Column | UUID Index |
|
||||||
|
|---|-------|-----------|------------|
|
||||||
|
| 1 | organizations | id | idx_organizations_uuid |
|
||||||
|
| 2 | projects | id | idx_projects_uuid |
|
||||||
|
| 3 | contracts | id | idx_contracts_uuid |
|
||||||
|
| 4 | users | user_id | idx_users_uuid |
|
||||||
|
| 5 | correspondences | id | idx_correspondences_uuid |
|
||||||
|
| 6 | correspondence_revisions | id | idx_correspondence_revisions_uuid |
|
||||||
|
| 7 | circulations | id | idx_circulations_uuid |
|
||||||
|
| 8 | shop_drawings | id | idx_shop_drawings_uuid |
|
||||||
|
| 9 | shop_drawing_revisions | id | idx_shop_drawing_revisions_uuid |
|
||||||
|
| 10 | contract_drawings | id | idx_contract_drawings_uuid |
|
||||||
|
| 11 | asbuilt_drawings | id | idx_asbuilt_drawings_uuid |
|
||||||
|
| 12 | asbuilt_drawing_revisions | id | idx_asbuilt_drawing_revisions_uuid |
|
||||||
|
| 13 | attachments | id | idx_attachments_uuid |
|
||||||
|
| 14 | notifications | id | idx_notifications_uuid |
|
||||||
|
|
||||||
|
### Excluded Tables (Shared-PK / Junction — inherit UUID from parent)
|
||||||
|
|
||||||
|
- `rfas` — shared PK with `correspondences`
|
||||||
|
- `rfa_revisions` — shared PK with `correspondence_revisions`
|
||||||
|
- `transmittals` — shared PK with `correspondences`
|
||||||
|
- `rfa_items` — junction table (composite PK, no own identity)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Backend — TypeORM Base Entity & UUID Utilities
|
||||||
|
|
||||||
|
> **Simplified by MariaDB Native UUID Type:** MariaDB 10.7+ stores UUID as `BINARY(16)` internally but auto-converts to/from string format. No manual binary conversion utilities or TypeORM transformers needed.
|
||||||
|
|
||||||
|
### 2.1 Create Base Entity with UUID
|
||||||
|
|
||||||
|
**File:** `backend/src/common/entities/uuid-base.entity.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Column, BeforeInsert } from 'typeorm';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
|
export abstract class UuidBaseEntity {
|
||||||
|
@Column({
|
||||||
|
type: 'uuid',
|
||||||
|
unique: true,
|
||||||
|
nullable: false,
|
||||||
|
comment: 'UUID Public Identifier (ADR-019)',
|
||||||
|
})
|
||||||
|
uuid: string;
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
generateUuid(): void {
|
||||||
|
if (!this.uuid) {
|
||||||
|
this.uuid = uuidv7();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** MariaDB native `UUID` type handles string ↔ binary conversion automatically.
|
||||||
|
> TypeORM reads/writes UUID as standard string format (8-4-4-4-12) — no transformer required.
|
||||||
|
> DB `DEFAULT UUID()` generates UUID v1 as fallback; app generates UUIDv7 via `@BeforeInsert()`.
|
||||||
|
|
||||||
|
### 2.2 Install uuid Package
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm install uuid
|
||||||
|
npm install -D @types/uuid
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Backend — Update Existing Entities
|
||||||
|
|
||||||
|
For each of the 14 public-facing entities, extend or mix in the UUID column:
|
||||||
|
|
||||||
|
### Pattern: Extend UuidBaseEntity
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example: correspondence.entity.ts
|
||||||
|
import { Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { UuidBaseEntity } from '../../common/entities/uuid-base.entity';
|
||||||
|
|
||||||
|
@Entity('correspondences')
|
||||||
|
export class Correspondence extends UuidBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
// ... existing columns (uuid + @BeforeInsert inherited from UuidBaseEntity)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entities to Update
|
||||||
|
|
||||||
|
| Entity File | Table |
|
||||||
|
|-------------|-------|
|
||||||
|
| `organization.entity.ts` | organizations |
|
||||||
|
| `project.entity.ts` | projects |
|
||||||
|
| `contract.entity.ts` | contracts |
|
||||||
|
| `user.entity.ts` | users |
|
||||||
|
| `correspondence.entity.ts` | correspondences |
|
||||||
|
| `correspondence-revision.entity.ts` | correspondence_revisions |
|
||||||
|
| `circulation.entity.ts` | circulations |
|
||||||
|
| `shop-drawing.entity.ts` | shop_drawings |
|
||||||
|
| `shop-drawing-revision.entity.ts` | shop_drawing_revisions |
|
||||||
|
| `contract-drawing.entity.ts` | contract_drawings |
|
||||||
|
| `asbuilt-drawing.entity.ts` | asbuilt_drawings |
|
||||||
|
| `asbuilt-drawing-revision.entity.ts` | asbuilt_drawing_revisions |
|
||||||
|
| `attachment.entity.ts` | attachments |
|
||||||
|
| `notification.entity.ts` | notifications |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Backend — API Layer Changes
|
||||||
|
|
||||||
|
### 4.1 UUID Pipe (Parameter Validation)
|
||||||
|
|
||||||
|
**File:** `backend/src/common/pipes/parse-uuid.pipe.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
|
||||||
|
import { validate as uuidValidate, version as uuidVersion } from 'uuid';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ParseUuidPipe implements PipeTransform<string> {
|
||||||
|
transform(value: string): string {
|
||||||
|
if (!uuidValidate(value) || uuidVersion(value) !== 7) {
|
||||||
|
throw new BadRequestException(`Invalid UUID: ${value}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Controller Pattern — UUID in URLs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BEFORE (INT): GET /api/correspondences/123
|
||||||
|
// AFTER (UUID): GET /api/correspondences/01912345-6789-7abc-...
|
||||||
|
|
||||||
|
@Get(':uuid')
|
||||||
|
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
|
||||||
|
return this.service.findByUuid(uuid);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Service Pattern — Internal UUID Lookup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async findByUuid(uuid: string): Promise<CorrespondenceDto> {
|
||||||
|
const entity = await this.repository.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
relations: ['revisions', 'recipients'],
|
||||||
|
});
|
||||||
|
if (!entity) throw new NotFoundException();
|
||||||
|
return this.mapToDto(entity);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 DTO Pattern — UUID Exposure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Response DTO exposes uuid, hides id
|
||||||
|
export class CorrespondenceResponseDto {
|
||||||
|
uuid: string; // ✅ Public identifier
|
||||||
|
correspondenceNumber: string;
|
||||||
|
// id: number; // ❌ Never expose INT id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 Migration Helper — findByUuidOrId
|
||||||
|
|
||||||
|
During transition, support both identifiers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async findByUuidOrId(identifier: string): Promise<Entity> {
|
||||||
|
const isUuid = uuidValidate(identifier);
|
||||||
|
if (isUuid) {
|
||||||
|
return this.repository.findOne({ where: { uuid: identifier } });
|
||||||
|
}
|
||||||
|
// Fallback to INT (internal/admin use only)
|
||||||
|
const id = parseInt(identifier, 10);
|
||||||
|
if (isNaN(id)) throw new BadRequestException();
|
||||||
|
return this.repository.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Frontend — UUID Integration
|
||||||
|
|
||||||
|
### 5.1 API Client Updates
|
||||||
|
|
||||||
|
- Update all API calls to use UUID in URL paths instead of INT id
|
||||||
|
- Update TanStack Query cache keys to use UUID
|
||||||
|
- Update Zustand stores to key by UUID
|
||||||
|
|
||||||
|
### 5.2 Route Parameters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BEFORE: /correspondences/[id]
|
||||||
|
// AFTER: /correspondences/[uuid]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Form Handling
|
||||||
|
|
||||||
|
- Hidden `uuid` field in forms for edit operations
|
||||||
|
- No changes needed for create operations (UUID generated server-side)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Testing & Verification
|
||||||
|
|
||||||
|
### 6.1 Unit Tests
|
||||||
|
|
||||||
|
- UUID generation produces valid UUIDv7
|
||||||
|
- UuidBaseEntity `@BeforeInsert()` auto-generates UUID when not provided
|
||||||
|
- ParseUuidPipe rejects invalid UUIDs
|
||||||
|
- MariaDB native UUID column stores and retrieves string format correctly
|
||||||
|
|
||||||
|
### 6.2 Integration Tests
|
||||||
|
|
||||||
|
- Entity creation auto-generates UUID
|
||||||
|
- API endpoints accept UUID parameters
|
||||||
|
- UUID lookup returns correct records
|
||||||
|
- Duplicate UUID detection (unique constraint)
|
||||||
|
|
||||||
|
### 6.3 Performance Verification
|
||||||
|
|
||||||
|
- Benchmark: UUID lookup via UNIQUE INDEX vs INT PK lookup
|
||||||
|
- Acceptable threshold: < 2x overhead on single-row lookups
|
||||||
|
- Verify B-tree ordering with time-sorted UUIDv7
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order (Priority)
|
||||||
|
|
||||||
|
| Order | Task | Effort | Depends On |
|
||||||
|
|-------|------|--------|------------|
|
||||||
|
| 1 | UuidBaseEntity (no transformer needed — MariaDB native UUID) | S | Phase 1 |
|
||||||
|
| 2 | Install `uuid` package | XS | — |
|
||||||
|
| 3 | Update 14 entity files with uuid column | M | Task 1 |
|
||||||
|
| 4 | Create ParseUuidPipe | S | — |
|
||||||
|
| 5 | Update controllers to use UUID params | L | Tasks 3, 4 |
|
||||||
|
| 6 | Update services with findByUuid methods | L | Task 3 |
|
||||||
|
| 7 | Update DTOs to expose uuid, hide id | M | Task 3 |
|
||||||
|
| 8 | Update frontend API calls | L | Tasks 5, 6, 7 |
|
||||||
|
| 9 | Update frontend routes | M | Task 8 |
|
||||||
|
| 10 | Write unit + integration tests | M | Tasks 1-7 |
|
||||||
|
ำ
|
||||||
|
**Estimated Total Effort:** ~3-5 days for backend, ~2-3 days for frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Strategy
|
||||||
|
|
||||||
|
If issues arise:
|
||||||
|
|
||||||
|
1. **Schema:** UUID columns have `DEFAULT` — existing inserts still work without app changes
|
||||||
|
2. **API:** INT-based endpoints can be restored by reverting controller/service changes
|
||||||
|
3. **Data:** No data loss — UUID column is additive (no existing columns modified)
|
||||||
|
4. **Frontend:** Route parameter changes are reversible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Seed files** do not need UUID values — the `DEFAULT UUID()` clause auto-generates UUIDs at INSERT time
|
||||||
|
- **Notifications table** uses a non-unique INDEX (not UNIQUE) for uuid because of its partitioned composite PK `(id, created_at)`
|
||||||
|
- **Workflow engine tables** (`workflow_instances`, `workflow_tasks`) already use `CHAR(36)` UUIDs — no changes needed
|
||||||
|
- **Shared-PK tables** (`rfas`, `rfa_revisions`, `transmittals`) inherit their parent's UUID via the correspondence relationship
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
# ADR-019: Hybrid Identifier Strategy (INT + UUIDv7)
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-03-12
|
||||||
|
**Version:** 1.8.1
|
||||||
|
**Decision Makers:** Development Team, Database Architect
|
||||||
|
**Related Documents:**
|
||||||
|
- [Data Dictionary](../03-Data-and-Storage/03-01-data-dictionary.md)
|
||||||
|
- [Database Schema](../03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql)
|
||||||
|
- [ADR-005: Technology Stack](ADR-005-technology-stack.md)
|
||||||
|
- [ADR-009: Database Migration Strategy](ADR-009-database-migration-strategy.md)
|
||||||
|
- [ADR-016: Security & Authentication](ADR-016-security-authentication.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
ระบบ LCBP3-DMS ใช้ `INT AUTO_INCREMENT` เป็น Primary Key ทุกตาราง ซึ่งทำงานได้ดีสำหรับ Internal JOIN/FK แต่มีปัญหาด้านความปลอดภัยและ Scalability:
|
||||||
|
|
||||||
|
1. **ID Enumeration Attack:** Sequential INT IDs ถูกเดาได้ง่าย (เช่น `/api/correspondences/1`, `/api/correspondences/2`) ทำให้ผู้ไม่ประสงค์ดีสามารถ Enumerate ข้อมูลได้
|
||||||
|
2. **Information Leakage:** INT IDs เปิดเผยจำนวนข้อมูลในระบบ (เช่น `user_id=5` แปลว่ามีผู้ใช้ 5 คน)
|
||||||
|
3. **Cross-System Integration:** หากในอนาคตต้องการ Sync ข้อมูลข้ามระบบ INT ID จะชนกัน
|
||||||
|
4. **API Security:** OWASP BOLA (Broken Object Level Authorization) แนะนำให้ใช้ Opaque Identifier แทน Sequential ID
|
||||||
|
|
||||||
|
ทั้งนี้ ระบบมีข้อจำกัดด้าน Hardware (QNAP NAS) ที่ต้องพิจารณาเรื่อง Performance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision Drivers
|
||||||
|
|
||||||
|
- **Security:** ป้องกัน ID Enumeration และลดความเสี่ยง OWASP BOLA
|
||||||
|
- **Performance:** INT PK ยังคงเป็น Primary Key เพื่อ JOIN/FK Performance บน InnoDB
|
||||||
|
- **Backward Compatibility:** ไม่ต้อง Migrate ข้อมูลหรือเปลี่ยน FK Relationships ที่มีอยู่
|
||||||
|
- **Simplicity:** เปลี่ยนเฉพาะ Public-Facing Tables (ไม่ใช่ทุกตาราง)
|
||||||
|
- **Standards:** UUIDv7 (RFC 9562) เป็น Time-ordered UUID ที่ B-tree friendly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
### Option 1: Replace INT with UUID as Primary Key
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ Opaque identifier ทุกที่
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- ❌ FK ทั้งหมดต้องเปลี่ยนเป็น BINARY(16) — Migration ซับซ้อนมาก
|
||||||
|
- ❌ JOIN Performance แย่ลง (16 bytes vs 4 bytes)
|
||||||
|
- ❌ InnoDB Clustered Index ไม่เรียงลำดับตาม INSERT Time (UUIDv4)
|
||||||
|
- ❌ ต้อง Rewrite Backend ทั้งหมด (Entity, DTO, Controller, Service)
|
||||||
|
- ❌ Breaking Change กับ Frontend ที่ใช้ INT ID อยู่
|
||||||
|
|
||||||
|
### Option 2: UUID as String Column (CHAR(36))
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ Human-readable
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- ❌ ใช้พื้นที่ 36 bytes ต่อ row (vs 16 bytes สำหรับ BINARY)
|
||||||
|
- ❌ Index ใหญ่ ช้ากว่า BINARY(16) อย่างมีนัยสำคัญ
|
||||||
|
- ❌ Collation issues กับ case-sensitivity
|
||||||
|
|
||||||
|
### Option 3: Hybrid INT + UUID (MariaDB Native) ⭐ (Selected)
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- ✅ INT PK ยังเป็น Internal ID → Performance ไม่เปลี่ยน
|
||||||
|
- ✅ UUID เป็น External ID → ปลอดภัย + Space-efficient (BINARY(16) ภายใน)
|
||||||
|
- ✅ ไม่ต้อง Migrate FK Relationships
|
||||||
|
- ✅ UUIDv7 Time-ordered → B-tree friendly, Index Performance ดี
|
||||||
|
- ✅ Backward Compatible — Frontend ค่อยๆ Migrate ได้
|
||||||
|
- ✅ ไม่กระทบ Migration Tables (Temporary)
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- ❌ ต้องเพิ่ม Column ใหม่ + UNIQUE INDEX ทุก Public-Facing Table
|
||||||
|
- ❌ Application Layer ต้อง Generate UUIDv7 ตอน INSERT
|
||||||
|
- ❌ API Layer ต้อง Resolve UUID → INT สำหรับ Internal Queries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
**Chosen Option:** Option 3 — Hybrid INT + UUID (MariaDB Native Type)
|
||||||
|
|
||||||
|
**Rationale:** เป็นแนวทางที่ Balance ระหว่าง Security, Performance และ Migration Effort ดีที่สุด ไม่ต้อง Rewrite FK ทั้งหมด ไม่ต้อง Migrate ข้อมูล และเพิ่มความปลอดภัยของ API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Specification
|
||||||
|
|
||||||
|
### 1. UUID Format
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| **Type** | MariaDB Native `UUID` (available since 10.7) |
|
||||||
|
| **Storage** | `BINARY(16)` internally (automatic) |
|
||||||
|
| **DB Default** | `UUID()` — generates UUID v1 (time-based, fallback for seed data) |
|
||||||
|
| **App Generation** | NestJS `@BeforeInsert()` generates UUIDv7 (RFC 9562) for time-ordering |
|
||||||
|
| **Display** | Auto-converts to string format (8-4-4-4-12) — no conversion function needed |
|
||||||
|
| **Index** | `UNIQUE INDEX` on `uuid` column |
|
||||||
|
|
||||||
|
### 2. Column Specification
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Column definition for all public-facing tables (MariaDB 10.7+)
|
||||||
|
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
UNIQUE INDEX idx_{table}_uuid (uuid)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** MariaDB native `UUID` type เก็บเป็น `BINARY(16)` ภายใน แต่แสดงผลเป็น String format อัตโนมัติ ไม่ต้องใช้ `BIN_TO_UUID()` / `UUID_TO_BIN()`
|
||||||
|
>
|
||||||
|
> **DB Default:** `UUID()` สร้าง UUID v1 (สำหรับ Seed Data และ Fallback)
|
||||||
|
>
|
||||||
|
> **Application Override:** NestJS Entity จะ Generate UUIDv7 เองก่อน INSERT เพื่อให้ได้ True UUIDv7 (Time-ordered, B-tree friendly)
|
||||||
|
|
||||||
|
### 3. Tables Requiring UUID Column
|
||||||
|
|
||||||
|
#### Tier 1 — Core Entity Tables (Own UUID Column)
|
||||||
|
|
||||||
|
| # | Table Name | Current PK | UUID Column | Notes |
|
||||||
|
|---|-----------|-----------|-------------|-------|
|
||||||
|
| 1 | `users` | `user_id INT AI` | `uuid UUID` | User profiles |
|
||||||
|
| 2 | `organizations` | `id INT AI` | `uuid UUID` | Organization data |
|
||||||
|
| 3 | `projects` | `id INT AI` | `uuid UUID` | Project data |
|
||||||
|
| 4 | `contracts` | `id INT AI` | `uuid UUID` | Contract data |
|
||||||
|
| 5 | `correspondences` | `id INT AI` | `uuid UUID` | Main document entity |
|
||||||
|
| 6 | `correspondence_revisions` | `id INT AI` | `uuid UUID` | Document versions |
|
||||||
|
| 7 | `circulations` | `id INT AI` | `uuid UUID` | Internal circulations |
|
||||||
|
| 8 | `shop_drawings` | `id INT AI` | `uuid UUID` | Shop drawing master |
|
||||||
|
| 9 | `shop_drawing_revisions` | `id INT AI` | `uuid UUID` | Shop drawing versions |
|
||||||
|
| 10 | `contract_drawings` | `id INT AI` | `uuid UUID` | Contract drawing master |
|
||||||
|
| 11 | `asbuilt_drawings` | `id INT AI` | `uuid UUID` | As-built drawing master |
|
||||||
|
| 12 | `asbuilt_drawing_revisions` | `id INT AI` | `uuid UUID` | As-built drawing versions |
|
||||||
|
| 13 | `attachments` | `id INT AI` | `uuid UUID` | File attachments |
|
||||||
|
| 14 | `notifications` | `id INT AI` (partitioned) | `uuid UUID` | User notifications |
|
||||||
|
|
||||||
|
#### Tier 2 — Shared-PK Tables (Inherit UUID from Parent)
|
||||||
|
|
||||||
|
| # | Table Name | Shared PK Source | UUID Resolution |
|
||||||
|
|---|-----------|-----------------|-----------------|
|
||||||
|
| 1 | `rfas` | `correspondences.id` | Use `correspondences.uuid` |
|
||||||
|
| 2 | `rfa_revisions` | `correspondence_revisions.id` | Use `correspondence_revisions.uuid` |
|
||||||
|
| 3 | `transmittals` | `correspondences.id` | Use `correspondences.uuid` |
|
||||||
|
|
||||||
|
#### Already Using UUID — No Changes Needed
|
||||||
|
|
||||||
|
| Table Name | Current PK |
|
||||||
|
|-----------|-----------|
|
||||||
|
| `workflow_definitions` | `CHAR(36) UUID` |
|
||||||
|
| `workflow_instances` | `CHAR(36) UUID` |
|
||||||
|
| `workflow_histories` | `CHAR(36) UUID` |
|
||||||
|
|
||||||
|
#### Excluded Tables (Internal/Master/Junction)
|
||||||
|
|
||||||
|
ตารางต่อไปนี้ **ไม่ต้อง** เพิ่ม UUID Column เพราะเป็น Internal-use only:
|
||||||
|
|
||||||
|
- **Master/Lookup:** `organization_roles`, `disciplines`, `correspondence_types`, `correspondence_sub_types`, `correspondence_status`, `rfa_types`, `rfa_status_codes`, `rfa_approve_codes`, `circulation_status_codes`, `tags`
|
||||||
|
- **RBAC:** `roles`, `permissions`, `user_assignments`
|
||||||
|
- **Junction/Mapping:** `project_organizations`, `contract_organizations`, `correspondence_recipients`, `correspondence_tags`, `correspondence_references`, `contract_drawing_subcat_cat_maps`, `shop_drawing_revision_contract_refs`, `asbuilt_revision_shop_revisions_refs`, all `*_attachments` junction tables
|
||||||
|
- **Drawing Categories:** `contract_drawing_volumes`, `contract_drawing_cats`, `contract_drawing_sub_cats`, `shop_drawing_main_categories`, `shop_drawing_sub_categories`
|
||||||
|
- **Document Numbering:** `document_number_formats`, `document_number_counters`, `document_number_audit`, `document_number_errors`, `document_number_reservations`
|
||||||
|
- **System/Logs:** `json_schemas`, `user_preferences`, `audit_logs`, `search_indices`, `backup_logs`, `refresh_tokens`
|
||||||
|
- **Migration (Temporary):** `migration_progress`, `migration_review_queue`, `migration_errors`, `migration_fallback_state`, `import_transactions`, `migration_daily_summary`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TypeORM Entity Pattern
|
||||||
|
|
||||||
|
### Base Entity with UUID
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/common/entities/base-uuid.entity.ts
|
||||||
|
import { Column, BeforeInsert } from 'typeorm';
|
||||||
|
import { v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
|
export abstract class BaseUuidEntity {
|
||||||
|
@Column({
|
||||||
|
type: 'uuid',
|
||||||
|
unique: true,
|
||||||
|
nullable: false,
|
||||||
|
comment: 'UUID Public Identifier (ADR-019)',
|
||||||
|
})
|
||||||
|
uuid!: string;
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
generateUuid(): void {
|
||||||
|
if (!this.uuid) {
|
||||||
|
this.uuid = uuidv7();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** MariaDB native `UUID` type ทำให้ TypeORM ไม่ต้องใช้ transformer อีกต่อไป — ค่าเข้า/ออกเป็น string format เสมอ
|
||||||
|
|
||||||
|
### Entity Usage Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example: correspondence.entity.ts
|
||||||
|
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||||
|
import { BaseUuidEntity } from '../../common/entities/base-uuid.entity';
|
||||||
|
|
||||||
|
@Entity('correspondences')
|
||||||
|
export class Correspondence extends BaseUuidEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
// ... existing columns
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Layer Changes
|
||||||
|
|
||||||
|
### URL Pattern
|
||||||
|
|
||||||
|
```
|
||||||
|
// Before (INT — vulnerable to enumeration)
|
||||||
|
GET /api/correspondences/42
|
||||||
|
GET /api/users/5
|
||||||
|
|
||||||
|
// After (UUID — opaque identifier)
|
||||||
|
GET /api/correspondences/019505a1-7c3e-7000-8000-abc123def456
|
||||||
|
GET /api/users/019505a1-8b2f-7000-8000-abc123def456
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controller Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Get(':uuid')
|
||||||
|
async findOne(@Param('uuid', ParseUUIDPipe) uuid: string) {
|
||||||
|
return this.service.findByUuid(uuid);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async findByUuid(uuid: string): Promise<CorrespondenceDto> {
|
||||||
|
const entity = await this.repository.findOne({
|
||||||
|
where: { uuid },
|
||||||
|
});
|
||||||
|
if (!entity) throw new NotFoundException();
|
||||||
|
return this.toDto(entity);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DTO Pattern — Never Expose INT ID
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class CorrespondenceResponseDto {
|
||||||
|
// ✅ Expose UUID as 'id' in API response
|
||||||
|
@Expose({ name: 'id' })
|
||||||
|
uuid!: string;
|
||||||
|
|
||||||
|
// ❌ Never expose internal INT id
|
||||||
|
// id: number; — REMOVED from response
|
||||||
|
|
||||||
|
// ... other fields
|
||||||
|
// For FK references, also use UUID
|
||||||
|
@Expose({ name: 'project_id' })
|
||||||
|
projectUuid!: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration SQL Script
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- =====================================================
|
||||||
|
-- ADR-019: Add UUIDv7 columns to public-facing tables
|
||||||
|
-- Strategy: Non-destructive — ADD COLUMN only
|
||||||
|
-- Rollback: DROP COLUMN uuid
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Tier 1: Core Entity Tables
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_users_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_organizations_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE projects
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_projects_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE contracts
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_contracts_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE correspondences
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_correspondences_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE correspondence_revisions
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_correspondence_revisions_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE circulations
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_circulations_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE shop_drawings
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_shop_drawings_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE shop_drawing_revisions
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_shop_drawing_revisions_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE contract_drawings
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_contract_drawings_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE asbuilt_drawings
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_asbuilt_drawings_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE asbuilt_drawing_revisions
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_asbuilt_drawing_revisions_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE attachments
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD UNIQUE INDEX idx_attachments_uuid (uuid);
|
||||||
|
|
||||||
|
ALTER TABLE notifications
|
||||||
|
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
|
||||||
|
COMMENT 'UUID Public Identifier (ADR-019)',
|
||||||
|
ADD INDEX idx_notifications_uuid (uuid);
|
||||||
|
-- Note: UNIQUE constraint on partitioned table requires uuid in partition key
|
||||||
|
-- Using regular INDEX instead
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback SQL
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Rollback: Remove UUID columns (Non-destructive reverse)
|
||||||
|
ALTER TABLE users DROP INDEX idx_users_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE organizations DROP INDEX idx_organizations_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE projects DROP INDEX idx_projects_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE contracts DROP INDEX idx_contracts_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE correspondences DROP INDEX idx_correspondences_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE correspondence_revisions DROP INDEX idx_correspondence_revisions_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE circulations DROP INDEX idx_circulations_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE shop_drawings DROP INDEX idx_shop_drawings_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE shop_drawing_revisions DROP INDEX idx_shop_drawing_revisions_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE contract_drawings DROP INDEX idx_contract_drawings_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE asbuilt_drawings DROP INDEX idx_asbuilt_drawings_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE asbuilt_drawing_revisions DROP INDEX idx_asbuilt_drawing_revisions_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE attachments DROP INDEX idx_attachments_uuid, DROP COLUMN uuid;
|
||||||
|
ALTER TABLE notifications DROP INDEX idx_notifications_uuid, DROP COLUMN uuid;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Storage Impact Analysis
|
||||||
|
|
||||||
|
| Item | Size |
|
||||||
|
|------|------|
|
||||||
|
| UUID (BINARY(16) internal) per row | 16 bytes |
|
||||||
|
| UNIQUE INDEX per row | ~16 bytes (key) + ~6 bytes (pointer) ≈ 22 bytes |
|
||||||
|
| **Total per row** | **~38 bytes** |
|
||||||
|
| Estimated rows (all 14 tables combined) | ~100,000 (Year 1) |
|
||||||
|
| **Total additional storage** | **~3.8 MB** |
|
||||||
|
| Impact on QNAP NAS | **Negligible** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### UUIDv7 vs UUIDv4 for B-tree Index
|
||||||
|
|
||||||
|
| Property | UUIDv4 | UUIDv7 |
|
||||||
|
|----------|--------|--------|
|
||||||
|
| Ordering | Random | Time-ordered |
|
||||||
|
| B-tree insert | Random page splits | Sequential append |
|
||||||
|
| Index fragmentation | High | Low |
|
||||||
|
| Cache efficiency | Poor | Good |
|
||||||
|
|
||||||
|
**UUIDv7 ถูกเลือกเพราะ Time-ordering** ทำให้ INSERT ไม่ทำให้เกิด Random Page Split บน InnoDB B-tree ซึ่งสำคัญมากสำหรับ QNAP NAS ที่มี I/O จำกัด
|
||||||
|
|
||||||
|
### Query Pattern
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Internal query (JOINs still use INT — no performance change)
|
||||||
|
SELECT c.*, cr.*
|
||||||
|
FROM correspondences c
|
||||||
|
JOIN correspondence_revisions cr ON c.id = cr.correspondence_id
|
||||||
|
WHERE c.id = 42;
|
||||||
|
|
||||||
|
-- API query (UUID lookup via UNIQUE INDEX — O(log n))
|
||||||
|
SELECT c.*, cr.*
|
||||||
|
FROM correspondences c
|
||||||
|
JOIN correspondence_revisions cr ON c.id = cr.correspondence_id
|
||||||
|
WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Benefits
|
||||||
|
|
||||||
|
| Threat | Before (INT) | After (Hybrid) |
|
||||||
|
|--------|-------------|----------------|
|
||||||
|
| ID Enumeration | ❌ Vulnerable (`/api/users/1,2,3...`) | ✅ Opaque UUID |
|
||||||
|
| Record Count Leak | ❌ `id=500` reveals ~500 records | ✅ UUID reveals nothing |
|
||||||
|
| BOLA Attack Surface | ❌ Predictable IDs | ✅ 2^122 possible values |
|
||||||
|
| Cross-System Collision | ❌ Possible | ✅ Globally unique |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compatibility with Existing ADRs
|
||||||
|
|
||||||
|
| ADR | Impact | Notes |
|
||||||
|
|-----|--------|-------|
|
||||||
|
| ADR-002 (Doc Numbering) | ✅ None | Document numbers (VARCHAR) are business identifiers, unaffected |
|
||||||
|
| ADR-005 (Tech Stack) | ✅ Compatible | uuid npm package + MariaDB native UUID type |
|
||||||
|
| ADR-006 (Redis Caching) | ⚠️ Minor | Cache keys should use UUID instead of INT for public-facing data |
|
||||||
|
| ADR-009 (DB Migration) | ✅ Compatible | ADD COLUMN is a safe, non-destructive migration |
|
||||||
|
| ADR-016 (Security) | ✅ Enhanced | Strengthens OWASP BOLA defense |
|
||||||
|
| ADR-017 (Ollama Migration) | ✅ None | Migration tables are temporary, excluded from UUID scope |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transition Strategy
|
||||||
|
|
||||||
|
### Phase 1: Database (Schema Change)
|
||||||
|
- เพิ่ม `uuid UUID` column (MariaDB native type) กับ UNIQUE INDEX ใน 14 ตาราง
|
||||||
|
- Existing rows ได้รับ UUID อัตโนมัติจาก DB DEFAULT
|
||||||
|
|
||||||
|
### Phase 2: Backend (Dual-Mode)
|
||||||
|
- เพิ่ม `uuid` field ใน TypeORM Entities
|
||||||
|
- สร้าง `BaseUuidEntity` class
|
||||||
|
- API รับได้ทั้ง INT และ UUID ผ่าน `FindByIdOrUuid` pattern
|
||||||
|
- API Response รวม UUID เป็น `id` field
|
||||||
|
|
||||||
|
### Phase 3: Frontend (Gradual Migration)
|
||||||
|
- Frontend เปลี่ยนจากใช้ `id` (INT) เป็น `id` (UUID) ใน API response
|
||||||
|
- URL parameters เปลี่ยนเป็น UUID
|
||||||
|
- ไม่ต้อง Big-Bang migration — ค่อยๆ เปลี่ยนทีละ Module
|
||||||
|
|
||||||
|
### Phase 4: Cleanup
|
||||||
|
- ลบ INT ID จาก API Response (DTO)
|
||||||
|
- ลบ INT-based route handlers
|
||||||
|
- Update API Documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Assessment
|
||||||
|
|
||||||
|
| Area | Status |
|
||||||
|
|------|--------|
|
||||||
|
| Security | ✅ Eliminates ID enumeration |
|
||||||
|
| Performance | ✅ No impact on internal JOINs |
|
||||||
|
| Migration Risk | ✅ Low — ADD COLUMN only |
|
||||||
|
| Storage Impact | ✅ Negligible (~3.8 MB) |
|
||||||
|
| Backward Compatibility | ✅ Dual-mode transition |
|
||||||
|
| ADR Compliance | ✅ Compatible with all existing ADRs |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน ADR-019-implementation-plan.md*
|
||||||
Reference in New Issue
Block a user