diff --git a/.agents/skills/nestjs-best-practices/SKILL.md b/.agents/skills/nestjs-best-practices/SKILL.md index 15708f2..8c5b4ee 100644 --- a/.agents/skills/nestjs-best-practices/SKILL.md +++ b/.agents/skills/nestjs-best-practices/SKILL.md @@ -89,7 +89,7 @@ Reference these guidelines when: - `db-hybrid-identifier` - **CRITICAL** ADR-019: INT PK + UUID public API - `db-avoid-n-plus-one` - HIGH N+1 query prevention - `db-use-transactions` - HIGH Transaction management -- `db-use-migrations` - N/A **ADR-009**: No TypeORM migrations - use SQL files +- `db-no-typeorm-migrations` - **CRITICAL** ADR-009: No TypeORM migrations - use SQL files ### 8. API Design (MEDIUM) @@ -110,7 +110,86 @@ Reference these guidelines when: - `devops-use-logging` - Structured logging - `devops-graceful-shutdown` - Zero-downtime deployments -## How to Use +## NAP-DMS Project-Specific Rules (MUST FOLLOW) + +These rules override general NestJS best practices for the NAP-DMS project: + +### ADR-009: No TypeORM Migrations + +- **ห้ามสร้างไฟล์ migration ของ TypeORM** +- แก้ไข schema โดยตรงที่: `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` +- ใช้ n8n workflow สำหรับ data migration ถ้าจำเป็น + +### ADR-019: Hybrid Identifier Strategy (CRITICAL) + +```typescript +@Entity() +export class Project { + @PrimaryGeneratedColumn() + @Exclude() // ห้ามส่งออกทาง API + id: number; // INT AUTO_INCREMENT - internal only + + @Column({ type: 'uuid' }) + @Expose({ name: 'id' }) // ส่งออกเป็น 'id' ทาง API + publicId: string; // UUIDv7 - public API identifier +} +``` + +### Two-Phase File Upload + +```typescript +// Phase 1: Upload to temp +@Post('upload') +async uploadFile(@UploadedFile() file: Express.Multer.File) { + await this.virusScan(file); + const tempId = await this.fileStorage.saveToTemp(file); + return { temp_id: tempId, expires_at: addHours(new Date(), 24) }; +} + +// Phase 2: Commit in transaction +async createEntity(dto: CreateDto, tempIds: string[]) { + return this.dataSource.transaction(async (manager) => { + const entity = await manager.save(Entity, dto); + await this.fileStorage.commitFiles(tempIds, entity.id, manager); + return entity; + }); +} +``` + +### Idempotency Requirement + +- ทุก POST/PUT/PATCH ที่สำคัญต้องมี `Idempotency-Key` header +- ใช้ `IdempotencyInterceptor` ที่มีอยู่แล้ว + +### Document Numbering (Double-Lock) + +```typescript +async generateNextNumber(context: NumberingContext): Promise { + const lockKey = `doc_num:${context.projectId}:${context.typeId}`; + const lock = await this.redisLock.acquire(lockKey, 3000); + + try { + const counter = await this.counterRepo.findOne({ + where: context, + lock: { mode: 'optimistic' }, + }); + counter.last_number++; + return this.formatNumber(await this.counterRepo.save(counter)); + } finally { + await lock.release(); + } +} +``` + +### Anti-Patterns (ห้ามทำ) + +- ❌ ใช้ SQL Triggers สำหรับ business logic +- ❌ ใช้ `.env` ใน production (ใช้ Docker ENV) +- ❌ ใช้ `any` type (strict mode enforced) +- ❌ ใช้ `console.log` (ใช้ NestJS Logger) +- ❌ สร้างตาราง routing แยก (ใช้ Workflow Engine) + +--- Read individual rule files for detailed explanations and code examples: diff --git a/.agents/skills/next-best-practices/SKILL.md b/.agents/skills/next-best-practices/SKILL.md index 7662e75..35761e7 100644 --- a/.agents/skills/next-best-practices/SKILL.md +++ b/.agents/skills/next-best-practices/SKILL.md @@ -165,7 +165,162 @@ See [self-hosting.md](./self-hosting.md) for: - Cache handlers for multi-instance ISR - What works vs needs extra setup -## Debug Tricks +## NAP-DMS Project-Specific Rules (MUST FOLLOW) + +These rules are mandatory for the NAP-DMS LCBP3 frontend project: + +### State Management (บังคับใช้) + +**Server State - TanStack Query (React Query)** + +```tsx +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; + +// ❌ ห้ามใช้ useEffect โดยตรง +// ✅ ใช้ TanStack Query +export function useCorrespondences(projectId: string) { + return useQuery({ + queryKey: ['correspondences', projectId], + queryFn: () => correspondenceService.getAll(projectId), + staleTime: 5 * 60 * 1000, + }); +} +``` + +**Form State - React Hook Form + Zod** + +```tsx +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; + +const schema = z.object({ + title: z.string().min(1, 'กรุณาระบุหัวเรื่อง'), + projectUuid: z.string().uuid('กรุณาเลือกโปรเจกต์'), +}); + +const form = useForm({ + resolver: zodResolver(schema), +}); +``` + +### ADR-019 UUID Handling (CRITICAL) + +```tsx +// Interface ต้องมีทั้ง id และ publicId +interface Contract { + id?: number; // Internal (อาจ undefined) + publicId?: string; // UUID - ใช้ตัวนี้ + contractCode: string; +} + +// Select options - ใช้ pattern นี้เสมอ +const options = contracts.map((c) => ({ + label: `${c.contractName} (${c.contractCode})`, + value: String(c.publicId ?? c.id ?? ''), // fallback pattern + key: String(c.publicId ?? c.id ?? ''), +})); + +// ❌ ห้ามใช้ parseInt บน UUID +// const id = parseInt(projectId); // WRONG! + +// ✅ ส่ง UUID string ตรงๆ +apiClient.get(`/projects/${projectId}`); // projectId is UUID string +``` + +### Naming Conventions + +**Code Identifiers - ภาษาอังกฤษ** + +```tsx +// ✅ Correct +interface Correspondence { + documentNumber: string; + createdAt: string; +} + +// ❌ Wrong +interface เอกสาร { + เลขที่: string; +} +``` + +**Comments - ภาษาไทย** + +```tsx +// ✅ Correct - อธิบาย logic เป็นภาษาไทย +// ตรวจสอบว่ามีการระบุ projectUuid หรือไม่ +if (!data.projectUuid) { + throw new Error('กรุณาเลือกโปรเจกต์'); +} + +// ❌ Wrong - ห้ามใช้ภาษาอังกฤษใน comments +// Check if projectUuid is provided +``` + +### UI Components + +**บังคับใช้ shadcn/ui** + +```tsx +// ✅ Correct +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; + +// ❌ Wrong - ไม่สร้าง component เองถ้ามีใน shadcn +const MyButton = () => ; +``` + +### File Upload Pattern + +```tsx +import { useDropzone } from 'react-dropzone'; + +// Two-phase upload +const onDrop = useCallback(async (files: File[]) => { + // Phase 1: Upload to temp + const tempFiles = await Promise.all(files.map((file) => uploadService.uploadTemp(file))); + setTempIds(tempFiles.map((f) => f.tempId)); +}, []); + +// Phase 2: Commit on form submit +const onSubmit = async (data: FormData) => { + await correspondenceService.create({ + ...data, + tempFileIds, + }); +}; +``` + +### API Client Setup + +```typescript +// lib/api/client.ts +const apiClient = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_URL, + timeout: 30000, +}); + +// Auto-add Idempotency-Key +apiClient.interceptors.request.use((config) => { + if (['post', 'put', 'patch'].includes(config.method?.toLowerCase() || '')) { + config.headers['Idempotency-Key'] = uuidv4(); + } + return config; +}); +``` + +### Anti-Patterns (ห้ามทำ) + +- ❌ Fetch data ใน useEffect โดยตรง +- ❌ Props drilling ลึกเกิน 3 levels +- ❌ Inline styles (ใช้ Tailwind) +- ❌ console.log ใน production +- ❌ parseInt() บน UUID values +- ❌ ใช้ index เป็น key ใน list +- ❌ Snake_case ใน form field names (ใช้ camelCase) + +--- See [debug-tricks.md](./debug-tricks.md) for: diff --git a/.agents/workflows/review.md b/.agents/workflows/review.md new file mode 100644 index 0000000..37fc514 --- /dev/null +++ b/.agents/workflows/review.md @@ -0,0 +1,62 @@ +--- +auto_execution_mode: 0 +description: Review code changes for bugs, security issues, and improvements +--- + +You are a senior software engineer performing a thorough code review to identify potential bugs. + +Your task is to find all potential bugs and code improvements in the code changes. Focus on: + +1. Logic errors and incorrect behavior +2. Edge cases that aren't handled +3. Null/undefined reference issues +4. Race conditions or concurrency issues +5. Security vulnerabilities +6. Improper resource management or resource leaks +7. API contract violations +8. Incorrect caching behavior, including cache staleness issues, cache key-related bugs, incorrect cache invalidation, and ineffective caching +9. Violations of existing code patterns or conventions + +## 🔴 Tier 1 Critical Rules (CI Blockers) + +The following are **CI-blocking issues** that must be caught in code review. These align with project specs in `specs/05-Engineering-Guidelines/` and `specs/06-Decision-Records/`: + +### ADR-019: UUID Handling + +- **❌ NEVER use `parseInt()`, `Number()`, or `+` operator on UUID values** + - Example of violation: `parseInt(projectId)` where `projectId` is UUID string + - ✅ Correct: Use UUID string directly without conversion +- **❌ NEVER expose internal INT PK in API responses** + - API must expose only `publicId` (transformed to `id` via `@Expose()`) + - Verify DTOs have `@Exclude()` on `id: number` field + +### TypeScript Strict Rules + +- **❌ ZERO `any` types allowed** — use proper types or `unknown` + narrowing +- **❌ ZERO `console.log`** — must use NestJS `Logger` (backend) or remove (frontend) +- **❌ NO `req: any` in controllers** — use `RequestWithUser` typed interface + +### Database & Architecture + +- **❌ NO SQL Triggers for business logic** — use NestJS Service methods instead +- **❌ NO `.env` files in production** — use Docker environment variables +- **❌ NO direct table/column name invention** — verify against `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` + +### Security (ADR-016) + +- Idempotency validation for critical `POST`/`PUT`/`PATCH` endpoints +- Two-phase file upload pattern (Upload → Temp → Commit → Permanent) +- Input validation with class-validator (backend) and Zod (frontend) + +### Test Coverage Requirements + +- **Backend Services:** 80% minimum +- **Backend Overall:** 70% minimum +- **Business Logic:** 80% minimum + +Make sure to: + +1. If exploring the codebase, call multiple tools in parallel for increased efficiency. Do not spend too much time exploring. +2. If you find any pre-existing bugs in the code, you should also report those since it's important for us to maintain general code quality for the user. +3. Do NOT report issues that are speculative or low-confidence. All your conclusions should be based on a complete understanding of the codebase. +4. Remember that if you were given a specific git commit, it may not be checked out and local code states may be different. diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index 99be2ac..9a33eb1 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -88,8 +88,29 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth | ---------------- | ------------------------- | ------------------------------------------- | | Internal / DB FK | `INT AUTO_INCREMENT` | Never exposed in API | | Public API / URL | `UUIDv7` (MariaDB native) | Stored as BINARY(16), no transformer needed | -| Entity Property | `publicId: string` | Exposed via `@Expose({ name: 'id' })` | -| API Response | `id: string` (UUID) | INT `id` has `@Exclude()` — never appears | +| Entity Property | `publicId: string` | Exposed directly in API (no transformation) | +| API Response | `publicId: string` (UUID) | INT `id` has `@Exclude()` — never appears | + +### ✅ Updated Pattern (March 2026) + +**Backend:** `UuidBaseEntity` exposes `publicId` directly — no `@Expose({ name: 'id' })` transformation + +**Frontend:** Use `publicId` only — no `uuid` or `id` fallbacks: + +```typescript +// ✅ CORRECT — Use publicId only +type ProjectOption = { + publicId?: string; // No uuid, no id fallback + projectName?: string; +}; + +// ❌ WRONG — Multiple identifiers cause confusion +type ProjectOption = { + publicId?: string; + uuid?: string; // Don't do this + id?: number; // Don't do this +}; +``` ### ❌ Forbidden UUID Patterns @@ -97,8 +118,11 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth // ❌ NEVER use parseInt on UUID parseInt(projectId); // "0195..." → 19 (WRONG!) -// ✅ CORRECT — Use UUID string directly -const id = projectId; // "019505a1-7c3e-7000-8000-abc123def456" +// ❌ NEVER use id ?? '' fallback +const value = c.publicId ?? c.id ?? ''; // Wrong! + +// ✅ CORRECT — Use publicId only +const value = c.publicId; // "019505a1-7c3e-7000-8000-abc123def456" ``` Read `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` before any UUID-related work. diff --git a/.vscode/settings.json b/.vscode/settings.json index 6f19320..7a44c21 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "editor.fontSize": 20, + "editor.fontSize": 16, "npm.packageManager": "pnpm" } diff --git a/.windsurf/workflows/review.md b/.windsurf/workflows/review.md index 7fb2fd4..37fc514 100644 --- a/.windsurf/workflows/review.md +++ b/.windsurf/workflows/review.md @@ -17,6 +17,43 @@ Your task is to find all potential bugs and code improvements in the code change 8. Incorrect caching behavior, including cache staleness issues, cache key-related bugs, incorrect cache invalidation, and ineffective caching 9. Violations of existing code patterns or conventions +## 🔴 Tier 1 Critical Rules (CI Blockers) + +The following are **CI-blocking issues** that must be caught in code review. These align with project specs in `specs/05-Engineering-Guidelines/` and `specs/06-Decision-Records/`: + +### ADR-019: UUID Handling + +- **❌ NEVER use `parseInt()`, `Number()`, or `+` operator on UUID values** + - Example of violation: `parseInt(projectId)` where `projectId` is UUID string + - ✅ Correct: Use UUID string directly without conversion +- **❌ NEVER expose internal INT PK in API responses** + - API must expose only `publicId` (transformed to `id` via `@Expose()`) + - Verify DTOs have `@Exclude()` on `id: number` field + +### TypeScript Strict Rules + +- **❌ ZERO `any` types allowed** — use proper types or `unknown` + narrowing +- **❌ ZERO `console.log`** — must use NestJS `Logger` (backend) or remove (frontend) +- **❌ NO `req: any` in controllers** — use `RequestWithUser` typed interface + +### Database & Architecture + +- **❌ NO SQL Triggers for business logic** — use NestJS Service methods instead +- **❌ NO `.env` files in production** — use Docker environment variables +- **❌ NO direct table/column name invention** — verify against `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql` + +### Security (ADR-016) + +- Idempotency validation for critical `POST`/`PUT`/`PATCH` endpoints +- Two-phase file upload pattern (Upload → Temp → Commit → Permanent) +- Input validation with class-validator (backend) and Zod (frontend) + +### Test Coverage Requirements + +- **Backend Services:** 80% minimum +- **Backend Overall:** 70% minimum +- **Business Logic:** 80% minimum + Make sure to: 1. If exploring the codebase, call multiple tools in parallel for increased efficiency. Do not spend too much time exploring. diff --git a/.windsurfrules b/.windsurfrules index 43e58a7..9fb46c7 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -1,7 +1,7 @@ # NAP-DMS Project Context & Rules - For: Windsurf Cascade (and compatible: Codex CLI, opencode, Amp, Amazon Q, AGENTS.md tools) - - Version: 1.8.5 (Refactored) | Last synced from repo: 2026-03-27 - - Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3) +- Version: 1.8.5 (Refactored) | Last synced from repo: 2026-03-27 +- Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3) --- @@ -84,8 +84,29 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth | ---------------- | ------------------------- | ------------------------------------------- | | Internal / DB FK | `INT AUTO_INCREMENT` | Never exposed in API | | Public API / URL | `UUIDv7` (MariaDB native) | Stored as BINARY(16), no transformer needed | -| Entity Property | `publicId: string` | Exposed via `@Expose({ name: 'id' })` | -| API Response | `id: string` (UUID) | INT `id` has `@Exclude()` — never appears | +| Entity Property | `publicId: string` | Exposed directly in API (no transformation) | +| API Response | `publicId: string` (UUID) | INT `id` has `@Exclude()` — never appears | + +### ✅ Updated Pattern (March 2026) + +**Backend:** `UuidBaseEntity` exposes `publicId` directly — no `@Expose({ name: 'id' })` transformation + +**Frontend:** Use `publicId` only — no `uuid` or `id` fallbacks: + +```typescript +// ✅ CORRECT — Use publicId only +type ProjectOption = { + publicId?: string; // No uuid, no id fallback + projectName?: string; +}; + +// ❌ WRONG — Multiple identifiers cause confusion +type ProjectOption = { + publicId?: string; + uuid?: string; // Don't do this + id?: number; // Don't do this +}; +``` ### ❌ Forbidden UUID Patterns @@ -93,8 +114,11 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth // ❌ NEVER use parseInt on UUID parseInt(projectId); // "0195..." → 19 (WRONG!) -// ✅ CORRECT — Use UUID string directly -const id = projectId; // "019505a1-7c3e-7000-8000-abc123def456" +// ❌ NEVER use id ?? '' fallback +const value = c.publicId ?? c.id ?? ''; // Wrong! + +// ✅ CORRECT — Use publicId only +const value = c.publicId; // "019505a1-7c3e-7000-8000-abc123def456" ``` Read `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` before any UUID-related work. diff --git a/AGENTS.md b/AGENTS.md index c70159e..2f0426d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,8 +85,29 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth | ---------------- | ------------------------- | ------------------------------------------- | | Internal / DB FK | `INT AUTO_INCREMENT` | Never exposed in API | | Public API / URL | `UUIDv7` (MariaDB native) | Stored as BINARY(16), no transformer needed | -| Entity Property | `publicId: string` | Exposed via `@Expose({ name: 'id' })` | -| API Response | `id: string` (UUID) | INT `id` has `@Exclude()` — never appears | +| Entity Property | `publicId: string` | Exposed directly in API (no transformation) | +| API Response | `publicId: string` (UUID) | INT `id` has `@Exclude()` — never appears | + +### ✅ Updated Pattern (March 2026) + +**Backend:** `UuidBaseEntity` exposes `publicId` directly — no `@Expose({ name: 'id' })` transformation + +**Frontend:** Use `publicId` only — no `uuid` or `id` fallbacks: + +```typescript +// ✅ CORRECT — Use publicId only +type ProjectOption = { + publicId?: string; // No uuid, no id fallback + projectName?: string; +}; + +// ❌ WRONG — Multiple identifiers cause confusion +type ProjectOption = { + publicId?: string; + uuid?: string; // Don't do this + id?: number; // Don't do this +}; +``` ### ❌ Forbidden UUID Patterns @@ -94,8 +115,11 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth // ❌ NEVER use parseInt on UUID parseInt(projectId); // "0195..." → 19 (WRONG!) -// ✅ CORRECT — Use UUID string directly -const id = projectId; // "019505a1-7c3e-7000-8000-abc123def456" +// ❌ NEVER use id ?? '' fallback +const value = c.publicId ?? c.id ?? ''; // Wrong! + +// ✅ CORRECT — Use publicId only +const value = c.publicId; // "019505a1-7c3e-7000-8000-abc123def456" ``` Read `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` before any UUID-related work. diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json index 9e3a076..850dfaa 100644 --- a/backend/.vscode/settings.json +++ b/backend/.vscode/settings.json @@ -1,3 +1,4 @@ { - "npm.packageManager": "pnpm" + "npm.packageManager": "pnpm", + "editor.fontSize": 16 } diff --git a/backend/package.json b/backend/package.json index f82426b..4cd19f4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -76,7 +76,7 @@ "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.27", - "uuid": "^11.0.0", + "uuid": "^9.0.0", "winston": "^3.18.3", "zod": "^4.1.13" }, @@ -115,24 +115,14 @@ "typescript-eslint": "^8.57.1" }, "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], + "preset": "ts-jest", + "testEnvironment": "node", "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, "collectCoverageFrom": [ "**/*.(t|j)s" ], - "coverageDirectory": "../coverage", - "testEnvironment": "node", - "transformIgnorePatterns": [ - "node_modules/(?!(uuid)/)" - ] + "coverageDirectory": "../coverage" }, "main": "index.js", "directories": { diff --git a/backend/src/modules/user/entities/user.entity.ts b/backend/src/modules/user/entities/user.entity.ts index 6e85c4f..89a17c4 100644 --- a/backend/src/modules/user/entities/user.entity.ts +++ b/backend/src/modules/user/entities/user.entity.ts @@ -17,7 +17,7 @@ import { Organization } from '../../organization/entities/organization.entity'; import { UserAssignment } from './user-assignment.entity'; import { UserPreference } from './user-preference.entity'; import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity'; -import { Exclude, Expose } from 'class-transformer'; +import { Exclude } from 'class-transformer'; @Entity('users') export class User extends UuidBaseEntity { @@ -65,7 +65,6 @@ export class User extends UuidBaseEntity { organization?: Organization; // ADR-019: Expose UUID instead of INT ID - @Expose({ name: 'primaryOrganizationId' }) get primaryOrganizationPublicId(): string | undefined { return this.organization?.publicId; } diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json index 9e3a076..850dfaa 100644 --- a/frontend/.vscode/settings.json +++ b/frontend/.vscode/settings.json @@ -1,3 +1,4 @@ { - "npm.packageManager": "pnpm" + "npm.packageManager": "pnpm", + "editor.fontSize": 16 } diff --git a/frontend/components/rfas/form.tsx b/frontend/components/rfas/form.tsx index 936c2c5..fa7b291 100644 --- a/frontend/components/rfas/form.tsx +++ b/frontend/components/rfas/form.tsx @@ -20,7 +20,7 @@ import { useProjects } from '@/hooks/use-projects'; import { CreateRfaDto } from '@/types/dto/rfa/rfa.dto'; import { useState, useEffect, type FormEvent } from 'react'; import { correspondenceService } from '@/lib/services/correspondence.service'; -import { Contract, getContractPublicId } from '@/types/contract'; +import { Contract } from '@/types/contract'; const rfaSchema = z.object({ projectId: z.string().min(1, 'Project is required'), // ADR-019: UUID @@ -41,16 +41,12 @@ type RFAFormData = z.infer; type ProjectOption = { publicId?: string; - uuid?: string; // Legacy alias for publicId - id?: number; projectName?: string; projectCode?: string; }; type ContractOption = { publicId?: string; - uuid?: string; - id?: string; contractName?: string; name?: string; contractCode?: string; @@ -78,20 +74,19 @@ type CorrespondenceTypeOption = { }; type OrganizationOption = { - uuid?: string; - id?: number; + publicId?: string; organizationCode?: string; organizationName?: string; }; type SelectableDrawingOption = { - uuid?: string; + publicId?: string; drawingNumber?: string; title?: string; legacyDrawingNumber?: string; - currentRevisionUuid?: string; + currentRevisionPublicId?: string; currentRevision?: { - uuid?: string; + publicId?: string; revisionLabel?: string; revisionNumber?: number | string; title?: string; @@ -145,11 +140,11 @@ export function RFAForm() { const createMutation = useCreateRFA(); const { data: projectsData, isLoading: isLoadingProjects } = useProjects(); - const projects = dedupeByKey(extractArrayData(projectsData), (project) => project.publicId ?? project.uuid ?? project.id); + const projects = dedupeByKey(extractArrayData(projectsData), (project) => project.publicId); const { data: organizationsData, isLoading: isLoadingOrganizations } = useOrganizations({ isActive: true }); const organizations = dedupeByKey( extractArrayData(organizationsData), - (organization) => organization.uuid ?? organization.id + (organization) => organization.publicId ); const { data: correspondenceTypesData } = useCorrespondenceTypes(); const correspondenceTypes = extractArrayData(correspondenceTypesData); @@ -185,7 +180,7 @@ export function RFAForm() { const { data: contractsData, isLoading: isLoadingContracts } = useContracts(selectedProjectId); const contracts = dedupeByKey( extractArrayData(contractsData), - (contract) => contract.publicId ?? contract.uuid ?? contract.id + (contract) => contract.publicId ); const selectedContractId = watch('contractId'); @@ -196,27 +191,27 @@ export function RFAForm() { const [shopDrawingSearch, setShopDrawingSearch] = useState(''); const [shopDrawingPage, setShopDrawingPage] = useState(1); const { data: shopDrawingsData, isLoading: isLoadingShopDrawings } = useDrawings('SHOP', { - projectUuid: selectedProjectId || '', + projectPublicId: selectedProjectId || '', search: shopDrawingSearch, page: shopDrawingPage, limit: 10, }); const shopDrawings = dedupeByKey( extractArrayData(shopDrawingsData), - (drawing) => drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid ?? drawing.uuid + (drawing) => drawing.currentRevisionPublicId ?? drawing.currentRevision?.publicId ?? drawing.publicId ); const [asBuiltDrawingSearch, setAsBuiltDrawingSearch] = useState(''); const [asBuiltDrawingPage, setAsBuiltDrawingPage] = useState(1); const { data: asBuiltDrawingsData, isLoading: isLoadingAsBuiltDrawings } = useDrawings('AS_BUILT', { - projectUuid: selectedProjectId || '', + projectPublicId: selectedProjectId || '', search: asBuiltDrawingSearch, page: asBuiltDrawingPage, limit: 10, }); const asBuiltDrawings = dedupeByKey( extractArrayData(asBuiltDrawingsData), - (drawing) => drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid ?? drawing.uuid + (drawing) => drawing.currentRevisionPublicId ?? drawing.currentRevision?.publicId ?? drawing.publicId ); const selectedDisciplineId = watch('disciplineId'); @@ -381,7 +376,7 @@ export function RFAForm() { {projects.map((p) => { - const projectValue = getOptionValue(p.publicId ?? p.uuid ?? p.id); + const projectValue = getOptionValue(p.publicId); if (!projectValue) { return null; @@ -417,7 +412,7 @@ export function RFAForm() { {contracts.map((c) => { - const contractValue = getOptionValue(getContractPublicId(c) || c.uuid); + const contractValue = getOptionValue(c.publicId); if (!contractValue) { return null; @@ -499,7 +494,7 @@ export function RFAForm() { {organizations.map((organization) => { - const organizationValue = getOptionValue(organization.uuid ?? organization.id); + const organizationValue = getOptionValue(organization.publicId); if (!organizationValue) { return null; @@ -551,24 +546,24 @@ export function RFAForm() { )}
{shopDrawings.map((drawing) => { - const revisionUuid = drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid; + const revisionPublicId = drawing.currentRevisionPublicId ?? drawing.currentRevision?.publicId; - if (!revisionUuid) { + if (!revisionPublicId) { return null; } return (