From a723cae2441a0173baa32263f0a7f15ad319f01e Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 20 Mar 2026 21:26:23 +0700 Subject: [PATCH] 690320:2126 UUID agian by Claude Sonnet #02 --- backend/src/modules/user/user.service.ts | 6 +++- .../admin/access-control/users/page.tsx | 6 +--- .../admin/doc-control/contracts/page.tsx | 7 +++-- frontend/components/admin/user-dialog.tsx | 2 +- frontend/components/correspondences/form.tsx | 30 +++++++++---------- frontend/hooks/use-master-data.ts | 3 +- frontend/hooks/use-projects.ts | 3 +- frontend/lib/services/organization.service.ts | 8 +++-- frontend/lib/services/project.service.ts | 16 ++++++++-- frontend/types/circulation.ts | 6 ++-- frontend/types/correspondence.ts | 6 ++-- .../dto/circulation/create-circulation.dto.ts | 8 ++--- frontend/types/dto/contract/contract.dto.ts | 4 +-- .../create-correspondence.dto.ts | 14 ++++----- frontend/types/dto/rfa/rfa.dto.ts | 12 ++++---- .../types/dto/transmittal/transmittal.dto.ts | 8 ++--- frontend/types/dto/user/user.dto.ts | 2 +- frontend/types/rfa.ts | 4 +-- frontend/types/transmittal.ts | 10 +++---- frontend/types/user.ts | 15 +++++----- 20 files changed, 93 insertions(+), 77 deletions(-) diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index 10a5768..8b08aec 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -85,6 +85,7 @@ export class UserService { .leftJoinAndSelect('user.preference', 'preference') // Optional .leftJoinAndSelect('user.assignments', 'assignments') .leftJoinAndSelect('assignments.role', 'role') + .leftJoin('user.organization', 'organization') // ADR-019: expose uuid, not INT .select([ 'user.user_id', 'user.uuid', @@ -93,13 +94,15 @@ export class UserService { 'user.firstName', 'user.lastName', 'user.lineId', - 'user.primaryOrganizationId', 'user.isActive', 'user.createdAt', 'user.updatedAt', 'assignments.id', 'role.roleId', 'role.roleName', + 'organization.uuid', + 'organization.organizationCode', + 'organization.organizationName', ]); // Apply Filters @@ -165,6 +168,7 @@ export class UserService { 'assignments', 'assignments.role', 'assignments.role.permissions', + 'organization', // ADR-019: expose org.uuid, not INT primaryOrganizationId ], }); diff --git a/frontend/app/(admin)/admin/access-control/users/page.tsx b/frontend/app/(admin)/admin/access-control/users/page.tsx index d38f259..95cd424 100644 --- a/frontend/app/(admin)/admin/access-control/users/page.tsx +++ b/frontend/app/(admin)/admin/access-control/users/page.tsx @@ -92,11 +92,7 @@ export default function UsersPage() { { id: "organization", header: "Organization", - cell: ({ row }) => { - const orgId = row.original.primaryOrganizationId; - const org = (organizations as Organization[]).find((o) => (o.id ?? o.uuid) === orgId?.toString() || o.uuid === orgId?.toString()); - return org ? org.organizationCode : "-"; - }, + cell: ({ row }) => row.original.organization?.organizationCode ?? "-", }, { id: "roles", diff --git a/frontend/app/(admin)/admin/doc-control/contracts/page.tsx b/frontend/app/(admin)/admin/doc-control/contracts/page.tsx index ca1f40f..5b0ef68 100644 --- a/frontend/app/(admin)/admin/doc-control/contracts/page.tsx +++ b/frontend/app/(admin)/admin/doc-control/contracts/page.tsx @@ -50,7 +50,8 @@ import { SearchContractDto, CreateContractDto, UpdateContractDto } from "@/types import { AxiosError } from "axios"; interface Project { - id: string; // ADR-019: uuid exposed as 'id' + uuid: string; + id?: number; projectCode: string; projectName: string; } @@ -82,14 +83,14 @@ const contractSchema = z.object({ type ContractFormData = z.infer; const useContracts = (params?: SearchContractDto) => { - return useQuery({ + return useQuery({ queryKey: ['contracts', params], queryFn: () => contractService.getAll(params), }); }; const useProjectsList = () => { - return useQuery({ + return useQuery({ queryKey: ['projects-list'], queryFn: () => projectService.getAll(), }); diff --git a/frontend/components/admin/user-dialog.tsx b/frontend/components/admin/user-dialog.tsx index d120805..9dcd5d8 100644 --- a/frontend/components/admin/user-dialog.tsx +++ b/frontend/components/admin/user-dialog.tsx @@ -107,7 +107,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) { lastName: user.lastName, isActive: user.isActive, lineId: user.lineId || "", - primaryOrganizationId: user.primaryOrganizationId?.toString(), + primaryOrganizationId: user.organization?.uuid ?? undefined, roleIds: user.roles?.map((r: { roleId: number }) => r.roleId) || [], password: "", confirmPassword: "" diff --git a/frontend/components/correspondences/form.tsx b/frontend/components/correspondences/form.tsx index a349a70..dc9a273 100644 --- a/frontend/components/correspondences/form.tsx +++ b/frontend/components/correspondences/form.tsx @@ -18,9 +18,10 @@ import { FileUploadZone } from "@/components/custom/file-upload-zone"; import { useRouter } from "next/navigation"; import { Loader2 } from "lucide-react"; import { useCreateCorrespondence, useUpdateCorrespondence } from "@/hooks/use-correspondence"; -import { Organization } from "@/types/organization"; +import { Organization, Correspondence, CorrespondenceRevision } from "@/types/correspondence"; import { useOrganizations, useProjects, useCorrespondenceTypes, useDisciplines } from "@/hooks/use-master-data"; import { CreateCorrespondenceDto } from "@/types/dto/correspondence/create-correspondence.dto"; +import type { ProjectListItem } from "@/lib/services/project.service"; import { useState, useEffect } from "react"; import { correspondenceService } from "@/lib/services/correspondence.service"; @@ -42,7 +43,7 @@ const correspondenceSchema = z.object({ type FormData = z.infer; -export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, uuid?: string }) { +export function CorrespondenceForm({ initialData, uuid }: { initialData?: Correspondence, uuid?: string }) { const router = useRouter(); const createMutation = useCreateCorrespondence(); const updateMutation = useUpdateCorrespondence(); @@ -53,23 +54,20 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u const { data: correspondenceTypes, isLoading: isLoadingTypes } = useCorrespondenceTypes(); const { data: disciplines, isLoading: isLoadingDisciplines } = useDisciplines(); - // Extract initial values if editing - const currentRev = initialData?.revisions?.find((r: any) => r.isCurrent) || initialData?.revisions?.[0]; + // Extract initial values if editing — ADR-019: use nested relation UUIDs, never raw INT FK columns + const currentRev = initialData?.revisions?.find((r: CorrespondenceRevision) => r.isCurrent) ?? initialData?.revisions?.[0]; const defaultValues: Partial = { - projectId: initialData?.projectId ? String(initialData.projectId) : undefined, + projectId: initialData?.project?.uuid ?? undefined, documentTypeId: initialData?.correspondenceTypeId || undefined, disciplineId: initialData?.disciplineId || undefined, - subject: currentRev?.subject || currentRev?.title || "", + subject: currentRev?.subject || "", description: currentRev?.description || "", body: currentRev?.body || "", remarks: currentRev?.remarks || "", dueDate: currentRev?.dueDate ? new Date(currentRev.dueDate).toISOString().split('T')[0] : undefined, - fromOrganizationId: initialData?.originatorId ? String(initialData.originatorId) : undefined, - // Map initial recipient (TO) - Simplified for now - toOrganizationId: initialData?.recipients?.find((r: any) => r.recipientType === 'TO')?.recipientOrganizationId - ? String(initialData.recipients.find((r: any) => r.recipientType === 'TO').recipientOrganizationId) - : undefined, - importance: currentRev?.details?.importance || "NORMAL", + fromOrganizationId: initialData?.originator?.uuid ?? undefined, + toOrganizationId: initialData?.recipients?.find(r => r.recipientType === 'TO')?.recipientOrganization?.uuid ?? undefined, + importance: (currentRev?.details?.['importance'] as "NORMAL" | "HIGH" | "URGENT") ?? "NORMAL", }; const { @@ -219,8 +217,8 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u - {(projects || []).map((p: any) => ( - + {(projects || []).map((p: ProjectListItem) => ( + {p.projectName} ({p.projectCode}) ))} @@ -243,7 +241,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u - {(correspondenceTypes || []).map((t: any) => ( + {(correspondenceTypes || []).map((t: { id: number; typeName: string; typeCode: string }) => ( {t.typeName} ({t.typeCode}) @@ -267,7 +265,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u - {(disciplines || []).map((d: any) => ( + {(disciplines || []).map((d: { id: number; disciplineCode?: string; codeNameEn?: string }) => ( {d.codeNameEn || d.disciplineCode} diff --git a/frontend/hooks/use-master-data.ts b/frontend/hooks/use-master-data.ts index cac6b9f..eed181a 100644 --- a/frontend/hooks/use-master-data.ts +++ b/frontend/hooks/use-master-data.ts @@ -6,6 +6,7 @@ import { UpdateOrganizationDto, SearchOrganizationDto, } from '@/types/dto/organization/organization.dto'; +import { Organization } from '@/types/organization'; import { AxiosError } from 'axios'; import { organizationService } from '@/lib/services/organization.service'; import { projectService } from '@/lib/services/project.service'; @@ -19,7 +20,7 @@ export const masterDataKeys = { }; export function useOrganizations(params?: SearchOrganizationDto) { - return useQuery({ + return useQuery({ queryKey: [...masterDataKeys.organizations(), params], queryFn: () => organizationService.getAll(params), }); diff --git a/frontend/hooks/use-projects.ts b/frontend/hooks/use-projects.ts index 529c020..8a4f1ee 100644 --- a/frontend/hooks/use-projects.ts +++ b/frontend/hooks/use-projects.ts @@ -1,5 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { projectService } from '@/lib/services/project.service'; +import type { ProjectListItem } from '@/lib/services/project.service'; import { CreateProjectDto, UpdateProjectDto, SearchProjectDto } from '@/types/dto/project/project.dto'; import { toast } from 'sonner'; import { getApiErrorMessage } from '@/types/api-error'; @@ -11,7 +12,7 @@ export const projectKeys = { }; export function useProjects(params?: SearchProjectDto) { - return useQuery({ + return useQuery({ queryKey: projectKeys.list(params || {}), queryFn: () => projectService.getAll(params), }); diff --git a/frontend/lib/services/organization.service.ts b/frontend/lib/services/organization.service.ts index 579f331..e5cae8d 100644 --- a/frontend/lib/services/organization.service.ts +++ b/frontend/lib/services/organization.service.ts @@ -4,6 +4,7 @@ import { UpdateOrganizationDto, SearchOrganizationDto, } from "@/types/dto/organization/organization.dto"; +import { Organization } from "@/types/organization"; type ApiEnvelope = { data?: ApiEnvelope | T; @@ -36,8 +37,11 @@ export const organizationService = { * Get all organizations (supports filtering by projectId) * GET /organizations?projectId=1 */ - getAll: async (params?: SearchOrganizationDto) => { - const response = await apiClient.get("/organizations", { params }); + getAll: async (params?: SearchOrganizationDto): Promise => { + const response = await apiClient.get | Organization[]>( + "/organizations", + { params } + ); return unwrapArrayResponse(response.data); }, diff --git a/frontend/lib/services/project.service.ts b/frontend/lib/services/project.service.ts index 1c719db..ba14efd 100644 --- a/frontend/lib/services/project.service.ts +++ b/frontend/lib/services/project.service.ts @@ -6,6 +6,16 @@ import { SearchProjectDto } from "@/types/dto/project/project.dto"; +export interface ProjectListItem { + uuid: string; + id?: number; + projectCode: string; + projectName: string; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + type ApiEnvelope = { data?: ApiEnvelope | T; message?: string; @@ -39,9 +49,11 @@ export const projectService = { * ดึงรายการโครงการทั้งหมด (รองรับ Search & Pagination) * (เดิมคือ getAllProjects แต่ปรับให้รับ params ได้) */ - getAll: async (params?: SearchProjectDto) => { + getAll: async (params?: SearchProjectDto): Promise => { // GET /projects - const response = await apiClient.get("/projects", { params }); + const response = await apiClient.get< + ApiEnvelope | ProjectListItem[] + >("/projects", { params }); return unwrapArrayResponse(response.data); }, diff --git a/frontend/types/circulation.ts b/frontend/types/circulation.ts index 1780b9a..b7209d1 100644 --- a/frontend/types/circulation.ts +++ b/frontend/types/circulation.ts @@ -89,10 +89,10 @@ export interface CirculationListResponse { * DTO for creating a circulation */ export interface CreateCirculationDto { - correspondenceId: number | string; - projectId?: number | string; + correspondenceId: string; // ADR-019: UUID string only + projectId?: string; // ADR-019: UUID string only subject: string; - assigneeIds: (number | string)[]; + assigneeIds: string[]; // ADR-019: UUID string only remarks?: string; } diff --git a/frontend/types/correspondence.ts b/frontend/types/correspondence.ts index b85f9dc..c7b3a29 100644 --- a/frontend/types/correspondence.ts +++ b/frontend/types/correspondence.ts @@ -76,7 +76,7 @@ export interface Correspondence { } export interface CreateCorrespondenceDto { - projectId: number; + projectId: string; // ADR-019: UUID string only typeId: number; subTypeId?: number; disciplineId?: number; @@ -87,7 +87,7 @@ export interface CreateCorrespondenceDto { description?: string; details?: Record; isInternal?: boolean; - originatorId?: number; - recipients?: { organizationId: number; type: 'TO' | 'CC' }[]; + originatorId?: string; // ADR-019: UUID string only + recipients?: { organizationId: string; type: 'TO' | 'CC' }[]; // ADR-019: UUID string only attachments?: File[]; } diff --git a/frontend/types/dto/circulation/create-circulation.dto.ts b/frontend/types/dto/circulation/create-circulation.dto.ts index c9cc55b..937e064 100644 --- a/frontend/types/dto/circulation/create-circulation.dto.ts +++ b/frontend/types/dto/circulation/create-circulation.dto.ts @@ -1,14 +1,14 @@ // File: src/types/dto/circulation/create-circulation.dto.ts export interface CreateCirculationDto { - /** เอกสารต้นเรื่องที่จะเวียน (Correspondence ID or UUID) */ - correspondenceId: number | string; + /** UUID ของเอกสารต้นเรื่องที่จะเวียน (ADR-019: UUID string only) */ + correspondenceId: string; /** หัวข้อเรื่อง (Subject) */ subject: string; - /** รายชื่อ User ID/UUID ที่ต้องการส่งให้ (ผู้รับผิดชอบ) */ - assigneeIds: (number | string)[]; + /** UUID ของ User ที่ต้องการส่งให้ (ADR-019: UUID string only) */ + assigneeIds: string[]; /** หมายเหตุเพิ่มเติม (ถ้ามี) */ remarks?: string; diff --git a/frontend/types/dto/contract/contract.dto.ts b/frontend/types/dto/contract/contract.dto.ts index 5370669..eb9a78d 100644 --- a/frontend/types/dto/contract/contract.dto.ts +++ b/frontend/types/dto/contract/contract.dto.ts @@ -1,7 +1,7 @@ export interface CreateContractDto { contractCode: string; contractName: string; - projectId: number | string; + projectId: string; // ADR-019: UUID string only description?: string; startDate?: string; endDate?: string; @@ -11,7 +11,7 @@ export type UpdateContractDto = Partial; export interface SearchContractDto { search?: string; - projectId?: number | string; + projectId?: string; // ADR-019: UUID string only page?: number; limit?: number; } diff --git a/frontend/types/dto/correspondence/create-correspondence.dto.ts b/frontend/types/dto/correspondence/create-correspondence.dto.ts index b05cedb..e3f82ae 100644 --- a/frontend/types/dto/correspondence/create-correspondence.dto.ts +++ b/frontend/types/dto/correspondence/create-correspondence.dto.ts @@ -1,8 +1,8 @@ // File: src/types/dto/correspondence/create-correspondence.dto.ts export interface CreateCorrespondenceDto { - /** ID or UUID ของโครงการ */ - projectId: number | string; + /** UUID ของโครงการ (ADR-019: UUID string only) */ + projectId: string; /** ID ของประเภทเอกสาร (เช่น RFA, LETTER) */ typeId: number; @@ -34,11 +34,11 @@ export interface CreateCorrespondenceDto { /** เอกสารภายในหรือไม่ (True = ภายใน) */ isInternal?: boolean; - /** * ✅ Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง) - * ใช้กรณี Admin สร้างเอกสารแทนผู้อื่น + /** ✅ Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง) + * ใช้กรณี Admin สร้างเอกสารแทนผู้อื่น (ADR-019: UUID string only) */ - originatorId?: number | string; + originatorId?: string; - /** รายชื่อผู้รับ */ - recipients?: { organizationId: number | string; type: 'TO' | 'CC' }[]; + /** รายชื่อผู้รับ (ADR-019: UUID string only) */ + recipients?: { organizationId: string; type: 'TO' | 'CC' }[]; } diff --git a/frontend/types/dto/rfa/rfa.dto.ts b/frontend/types/dto/rfa/rfa.dto.ts index 7b2b316..6c368c1 100644 --- a/frontend/types/dto/rfa/rfa.dto.ts +++ b/frontend/types/dto/rfa/rfa.dto.ts @@ -3,8 +3,8 @@ import type { RFAItem } from '@/types/rfa'; // --- Create --- export interface CreateRfaDto { - /** ID or UUID ของโครงการ */ - projectId: number | string; // ADR-019: Accept UUID + /** UUID ของโครงการ (ADR-019: UUID string only) */ + projectId: string; /** ประเภท RFA (เช่น DWG, MAT) */ rfaTypeId: number; @@ -24,8 +24,8 @@ export interface CreateRfaDto { /** Contract UUID (optional) */ contractId?: string; // ADR-019: Contract UUID - /** ส่งถึงใคร (สำหรับ Routing Step 1) */ - toOrganizationId: number | string; // ADR-019: Accept UUID + /** ส่งถึงใคร (สำหรับ Routing Step 1) (ADR-019: UUID string only) */ + toOrganizationId: string; /** รายละเอียดเพิ่มเติม */ description?: string; @@ -48,8 +48,8 @@ export type UpdateRfaDto = Partial; // --- Search --- export interface SearchRfaDto { - /** Filter by Project ID or UUID (optional to allow cross-project search) */ - projectId?: number | string; // ADR-019: Accept UUID + /** Filter by Project UUID (ADR-019: UUID string only) */ + projectId?: string; /** กรองตามประเภท RFA */ rfaTypeId?: number; diff --git a/frontend/types/dto/transmittal/transmittal.dto.ts b/frontend/types/dto/transmittal/transmittal.dto.ts index d22dabe..a557983 100644 --- a/frontend/types/dto/transmittal/transmittal.dto.ts +++ b/frontend/types/dto/transmittal/transmittal.dto.ts @@ -9,12 +9,12 @@ export enum TransmittalPurpose { // --- Create --- export interface CreateTransmittalDto { - projectId?: number | string; // ADR-019: Accept UUID - recipientOrganizationId?: number | string; // ADR-019: Accept UUID + projectId?: string; // ADR-019: UUID string only + recipientOrganizationId?: string; // ADR-019: UUID string only subject: string; purpose?: string; remarks?: string; - correspondenceId: number | string; // ADR-019: Accept UUID + correspondenceId: string; // ADR-019: UUID string only items: CreateTransmittalItemDto[]; } @@ -30,7 +30,7 @@ export type UpdateTransmittalDto = Partial; // --- Search --- export interface SearchTransmittalDto { /** บังคับระบุ Project */ - projectId: number | string; // ADR-019: Accept UUID + projectId: string; // ADR-019: UUID string only purpose?: TransmittalPurpose; diff --git a/frontend/types/dto/user/user.dto.ts b/frontend/types/dto/user/user.dto.ts index f6ed982..5d590f3 100644 --- a/frontend/types/dto/user/user.dto.ts +++ b/frontend/types/dto/user/user.dto.ts @@ -8,7 +8,7 @@ export interface CreateUserDto { firstName?: string; lastName?: string; lineId?: string; - primaryOrganizationId?: number | string; // ADR-019: Accept UUID + primaryOrganizationId?: string; // ADR-019: UUID string only isActive?: boolean; } diff --git a/frontend/types/rfa.ts b/frontend/types/rfa.ts index 61a891b..7381f52 100644 --- a/frontend/types/rfa.ts +++ b/frontend/types/rfa.ts @@ -54,9 +54,9 @@ export interface RFA { } export interface CreateRFADto { - projectId: number | string; // ADR-019: Accept UUID + projectId: string; // ADR-019: UUID string only contractId?: string; // ADR-019: Contract UUID - toOrganizationId?: number | string; // ADR-019: Recipient org UUID + toOrganizationId?: string; // ADR-019: UUID string only rfaTypeId: number; disciplineId?: number; subject: string; diff --git a/frontend/types/transmittal.ts b/frontend/types/transmittal.ts index c5176f7..aa5729a 100644 --- a/frontend/types/transmittal.ts +++ b/frontend/types/transmittal.ts @@ -30,7 +30,7 @@ export interface TransmittalItem { export interface Transmittal { uuid: string; // ADR-019: from correspondence.uuid id?: number; // Excluded from API responses (ADR-019) - correspondenceId?: number | string; + correspondenceId?: string; // ADR-019: UUID string only transmittalNo: string; subject: string; purpose?: TransmittalPurpose; @@ -74,9 +74,9 @@ export interface CreateTransmittalItemDto { * DTO for creating a transmittal */ export interface CreateTransmittalDto { - projectId?: number | string; // ADR-019: Accept UUID - recipientOrganizationId?: number | string; // ADR-019: Accept UUID - correspondenceId: number | string; // ADR-019: Accept UUID + projectId?: string; // ADR-019: UUID string only + recipientOrganizationId?: string; // ADR-019: UUID string only + correspondenceId: string; // ADR-019: UUID string only subject: string; purpose?: TransmittalPurpose; remarks?: string; @@ -89,6 +89,6 @@ export interface CreateTransmittalDto { export interface SearchTransmittalDto { page?: number; limit?: number; - projectId?: number | string; // ADR-019: Accept UUID + projectId?: string; // ADR-019: UUID string only search?: string; } diff --git a/frontend/types/user.ts b/frontend/types/user.ts index 92ea8bb..0d9a2ea 100644 --- a/frontend/types/user.ts +++ b/frontend/types/user.ts @@ -5,10 +5,9 @@ export interface Role { } export interface UserOrganization { - organizationId: number; - orgCode: string; - orgName: string; - orgNameTh?: string; + uuid: string; // ADR-019: Public identifier + organizationCode: string; // Matches backend Organization entity + organizationName: string; // Matches backend Organization entity } export interface User { @@ -20,8 +19,8 @@ export interface User { lastName: string; isActive: boolean; lineId?: string; - primaryOrganizationId?: number | string; // ADR-019: May be INT or UUID - organization?: UserOrganization; + organization?: UserOrganization; // ADR-019: use organization.uuid — never expose INT id + roles?: Role[]; // Security fields (from backend v1.5.1) @@ -42,7 +41,7 @@ export interface CreateUserDto { password?: string; isActive: boolean; lineId?: string; - primaryOrganizationId?: number | string; // ADR-019: Accept UUID + primaryOrganizationId?: string; // ADR-019: UUID string only roleIds: number[]; } @@ -53,5 +52,5 @@ export interface SearchUserDto { limit?: number; search?: string; roleId?: number; - primaryOrganizationId?: number | string; // ADR-019: Accept UUID + primaryOrganizationId?: string; // ADR-019: UUID string only }