690327:0024 Fixing Refactor ADR-019 Naming convention uuid #11
CI / CD Pipeline / build (push) Successful in 6m35s
CI / CD Pipeline / deploy (push) Failing after 12m21s

This commit is contained in:
2026-03-27 00:24:16 +07:00
parent 50b6a0f901
commit 9c5ac74ce5
15 changed files with 72 additions and 42 deletions
@@ -17,7 +17,7 @@ import { Organization } from '../../organization/entities/organization.entity';
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 { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
import { Exclude } from 'class-transformer'; import { Exclude, Expose } from 'class-transformer';
@Entity('users') @Entity('users')
export class User extends UuidBaseEntity { export class User extends UuidBaseEntity {
@@ -57,12 +57,19 @@ export class User extends UuidBaseEntity {
// Relation กับ Organization (สังกัดหลัก) // Relation กับ Organization (สังกัดหลัก)
@Column({ name: 'primary_organization_id', nullable: true }) @Column({ name: 'primary_organization_id', nullable: true })
@Exclude() // INT ID - never expose, use primaryOrganizationPublicId instead (ADR-019)
primaryOrganizationId?: number; primaryOrganizationId?: number;
@ManyToOne(() => Organization, { nullable: true, onDelete: 'SET NULL' }) @ManyToOne(() => Organization, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'primary_organization_id' }) @JoinColumn({ name: 'primary_organization_id' })
organization?: Organization; organization?: Organization;
// ADR-019: Expose UUID instead of INT ID
@Expose({ name: 'primaryOrganizationId' })
get primaryOrganizationPublicId(): string | undefined {
return this.organization?.publicId;
}
// Relation กับ Assignments (RBAC) // Relation กับ Assignments (RBAC)
@OneToMany(() => UserAssignment, (assignment) => assignment.user) @OneToMany(() => UserAssignment, (assignment) => assignment.user)
assignments?: UserAssignment[]; assignments?: UserAssignment[];
+4 -1
View File
@@ -84,6 +84,7 @@ export class UserService {
.leftJoinAndSelect('user.preference', 'preference') // Optional .leftJoinAndSelect('user.preference', 'preference') // Optional
.leftJoinAndSelect('user.assignments', 'assignments') .leftJoinAndSelect('user.assignments', 'assignments')
.leftJoinAndSelect('assignments.role', 'role') .leftJoinAndSelect('assignments.role', 'role')
.leftJoinAndSelect('user.organization', 'organization') // [FIX] Required for primaryOrganizationPublicId getter (ADR-019)
.select([ .select([
'user.user_id', 'user.user_id',
'user.publicId', 'user.publicId',
@@ -92,13 +93,13 @@ export class UserService {
'user.firstName', 'user.firstName',
'user.lastName', 'user.lastName',
'user.lineId', 'user.lineId',
'user.primaryOrganizationId',
'user.isActive', 'user.isActive',
'user.createdAt', 'user.createdAt',
'user.updatedAt', 'user.updatedAt',
'assignments.id', 'assignments.id',
'role.roleId', 'role.roleId',
'role.roleName', 'role.roleName',
'organization.publicId', // [FIX] Expose org UUID for getter (ADR-019)
]); ]);
// Apply Filters // Apply Filters
@@ -146,6 +147,7 @@ export class UserService {
'assignments', 'assignments',
'assignments.role', 'assignments.role',
'assignments.role.permissions', // [FIX] Required for RBAC AbilityFactory 'assignments.role.permissions', // [FIX] Required for RBAC AbilityFactory
'organization', // [FIX] Required for primaryOrganizationPublicId getter (ADR-019)
], ],
}); });
@@ -164,6 +166,7 @@ export class UserService {
'assignments', 'assignments',
'assignments.role', 'assignments.role',
'assignments.role.permissions', 'assignments.role.permissions',
'organization', // [FIX] Required for primaryOrganizationPublicId getter (ADR-019)
], ],
}); });
@@ -38,7 +38,7 @@ import { SearchContractDto, CreateContractDto, UpdateContractDto } from '@/types
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
interface _Project { interface _Project {
id: string; // ADR-019: uuid exposed as 'id' (string) publicId: string; // ADR-019: uuid exposed as 'publicId' (string)
projectCode: string; projectCode: string;
projectName: string; projectName: string;
} }
@@ -206,8 +206,8 @@ export default function ContractsPage() {
const handleEdit = (contract: Contract) => { const handleEdit = (contract: Contract) => {
setEditingUuid(contract.id); setEditingUuid(contract.id);
// ADR-019: project.id is the project's UUID (exposed via @Expose) // ADR-019: project.publicId is the project's UUID
const pId = contract.project?.id || ''; const pId = contract.project?.publicId || '';
reset({ reset({
contractCode: contract.contractCode, contractCode: contract.contractCode,
contractName: contract.contractName, contractName: contract.contractName,
@@ -299,8 +299,8 @@ export default function ContractsPage() {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{projects?.map((p) => ( {projects?.map((p) => (
// ADR-019: Project exposes UUID as 'id' (string) // ADR-019: Project exposes UUID as 'publicId'
<SelectItem key={p.id} value={p.id}> <SelectItem key={p.publicId} value={p.publicId}>
{p.projectCode} - {p.projectName} {p.projectCode} - {p.projectName}
</SelectItem> </SelectItem>
))} ))}
@@ -67,8 +67,8 @@ export default function ContractCategoriesPage() {
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[]).map((project) => ( {(projects as { id?: number; publicId?: string; projectCode: string; projectName: string }[]).map((project) => (
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}> <SelectItem key={project.publicId || project.id} value={String(project.publicId || project.id)}>
{project.projectCode} - {project.projectName} {project.projectCode} - {project.projectName}
</SelectItem> </SelectItem>
))} ))}
@@ -59,8 +59,8 @@ export default function ContractSubCategoriesPage() {
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[]).map((project) => ( {(projects as { id?: number; publicId?: string; projectCode: string; projectName: string }[]).map((project) => (
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}> <SelectItem key={project.publicId || project.id} value={String(project.publicId || project.id)}>
{project.projectCode} - {project.projectName} {project.projectCode} - {project.projectName}
</SelectItem> </SelectItem>
))} ))}
@@ -59,8 +59,8 @@ export default function ContractVolumesPage() {
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[]).map((project) => ( {(projects as { id?: number; publicId?: string; projectCode: string; projectName: string }[]).map((project) => (
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}> <SelectItem key={project.publicId || project.id} value={String(project.publicId || project.id)}>
{project.projectCode} - {project.projectName} {project.projectCode} - {project.projectName}
</SelectItem> </SelectItem>
))} ))}
@@ -70,8 +70,8 @@ export default function ShopMainCategoriesPage() {
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[]).map((project) => ( {(projects as { id?: number; publicId?: string; projectCode: string; projectName: string }[]).map((project) => (
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}> <SelectItem key={project.publicId || project.id} value={String(project.publicId || project.id)}>
{project.projectCode} - {project.projectName} {project.projectCode} - {project.projectName}
</SelectItem> </SelectItem>
))} ))}
@@ -70,8 +70,8 @@ export default function ShopSubCategoriesPage() {
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(projects as { id?: number; uuid?: string; projectCode: string; projectName: string }[]).map((project) => ( {(projects as { id?: number; publicId?: string; projectCode: string; projectName: string }[]).map((project) => (
<SelectItem key={project.uuid || project.id} value={String(project.id || project.uuid)}> <SelectItem key={project.publicId || project.id} value={String(project.publicId || project.id)}>
{project.projectCode} - {project.projectName} {project.projectCode} - {project.projectName}
</SelectItem> </SelectItem>
))} ))}
@@ -24,12 +24,15 @@ export default function EditTemplatePage() {
const { data: correspondenceTypes = [] } = useCorrespondenceTypes(); const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
const { data: projects = [] } = useProjects(); const { data: projects = [] } = useProjects();
const projectId = template?.projectId || 1; const projectId = template?.projectId || 1;
const { data: contracts = [] } = useContracts(projectId); const { data: contractsData } = useContracts(projectId);
const contractId = contracts[0]?.id; const contracts = Array.isArray(contractsData) ? contractsData : [];
const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined;
const contractId = firstContract?.publicId ?? firstContract?.id;
const { data: disciplines = [] } = useDisciplines(contractId); const { data: disciplines = [] } = useDisciplines(contractId);
const selectedProjectName = const selectedProjectName =
projects.find((p: { id?: number; uuid?: string; projectCode: string; projectName: string }) => p.id === projectId) projects.find((p: { id?: number; publicId?: string; projectCode: string; projectName: string }) =>
String(p.publicId ?? p.id) === String(projectId))
?.projectName || 'LCBP3'; ?.projectName || 'LCBP3';
useEffect(() => { useEffect(() => {
@@ -14,12 +14,15 @@ export default function NewTemplatePage() {
const { data: correspondenceTypes = [] } = useCorrespondenceTypes(); const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
const { data: projects = [] } = useProjects(); const { data: projects = [] } = useProjects();
const projectId = 1; // Default or sync with selection const projectId = 1; // Default or sync with selection
const { data: contracts = [] } = useContracts(projectId); const { data: contractsData } = useContracts(projectId);
const contractId = contracts[0]?.id; const contracts = Array.isArray(contractsData) ? contractsData : [];
const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined;
const contractId = firstContract?.publicId ?? firstContract?.id;
const { data: disciplines = [] } = useDisciplines(contractId); const { data: disciplines = [] } = useDisciplines(contractId);
const selectedProjectName = const selectedProjectName =
projects.find((p: { id: number; projectName: string }) => p.id === projectId)?.projectName || 'LCBP3'; projects.find((p: { id?: number; publicId?: string; projectName: string }) =>
String(p.publicId ?? p.id) === String(projectId))?.projectName || 'LCBP3';
const handleSave = async (data: Partial<NumberingTemplate>) => { const handleSave = async (data: Partial<NumberingTemplate>) => {
try { try {
@@ -53,7 +53,8 @@ export default function NumberingPage() {
// Master Data // Master Data
const { data: correspondenceTypes = [] } = useCorrespondenceTypes(); const { data: correspondenceTypes = [] } = useCorrespondenceTypes();
const { data: contracts = [] } = useContracts(selectedProjectId); const { data: contractsData } = useContracts(selectedProjectId);
const contracts = Array.isArray(contractsData) ? contractsData : [];
const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined; const firstContract = contracts[0] as { id?: number; publicId?: string } | undefined;
const contractId = firstContract?.publicId ?? firstContract?.id; const contractId = firstContract?.publicId ?? firstContract?.id;
const { data: disciplines = [] } = useDisciplines(contractId); const { data: disciplines = [] } = useDisciplines(contractId);
@@ -147,8 +148,7 @@ export default function NumberingPage() {
.filter( .filter(
(t) => (t) =>
!t.projectId || !t.projectId ||
String(t.project?.id ?? t.project?.publicId) === selectedProjectId || String(t.project?.publicId) === selectedProjectId
t.project?.publicId === selectedProjectId
) )
.map((template) => ( .map((template) => (
<Card key={template.id} className="p-6 hover:shadow-md transition-shadow"> <Card key={template.id} className="p-6 hover:shadow-md transition-shadow">
@@ -33,7 +33,7 @@ import {
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
interface Project { interface Project {
id: string; // ADR-019: uuid exposed as 'id' publicId: string; // ADR-019: uuid exposed as 'publicId'
projectCode: string; projectCode: string;
projectName: string; projectName: string;
isActive: boolean; isActive: boolean;
@@ -71,7 +71,7 @@ export default function ProjectsPage() {
const confirmDelete = () => { const confirmDelete = () => {
if (projectToDelete) { if (projectToDelete) {
deleteProject.mutate(projectToDelete.id, { deleteProject.mutate(projectToDelete.publicId, {
onSuccess: () => { onSuccess: () => {
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setProjectToDelete(null); setProjectToDelete(null);
@@ -145,7 +145,7 @@ export default function ProjectsPage() {
]; ];
const handleEdit = (project: Project) => { const handleEdit = (project: Project) => {
setEditingUuid(project.id); setEditingUuid(project.publicId);
reset({ reset({
projectCode: project.projectCode, projectCode: project.projectCode,
projectName: project.projectName, projectName: project.projectName,
@@ -27,10 +27,10 @@ export default function TagsPage() {
accessorKey: 'project_id', accessorKey: 'project_id',
header: 'Project', header: 'Project',
cell: ({ row }) => { cell: ({ row }) => {
const item = row.original as Tag & { project?: { id?: number | string; projectName?: string; projectCode?: string } }; const item = row.original as Tag & { project?: { id?: number | string; publicId?: string; projectName?: string; projectCode?: string } };
const project = item.project; const project = item.project;
if (!project) return <span className="text-muted-foreground italic">Global</span>; if (!project) return <span className="text-muted-foreground italic">Global</span>;
return (project.projectName || project.projectCode || `Project ${project.id}`) as React.ReactNode; return (project.projectName || project.projectCode || `Project ${project.publicId || project.id}`) as React.ReactNode;
}, },
}, },
{ {
@@ -75,10 +75,10 @@ export default function TagsPage() {
const items = await masterDataService.getTags(); const items = await masterDataService.getTags();
// ADR-019: Map project_id INT → project UUID for edit mode select matching // ADR-019: Map project_id INT → project UUID for edit mode select matching
return items.map((item) => { return items.map((item) => {
const rec = item as Tag & { project?: { id?: number | string; uuid?: string }; project_id?: number | string }; const rec = item as Tag & { project?: { id?: number | string; publicId?: string }; project_id?: number | string };
return { return {
...item, ...item,
project_id: rec.project?.id || rec.project?.uuid || (rec.project_id ? String(rec.project_id) : null), project_id: rec.project?.publicId || rec.project?.id || (rec.project_id ? String(rec.project_id) : null),
} as Tag; } as Tag;
}); });
}} }}
+2 -2
View File
@@ -40,8 +40,8 @@ export default function DrawingsPage() {
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{projects.map((project: { id: string; projectName: string; projectCode: string }) => ( {projects.map((project: { id?: number; publicId?: string; projectName: string; projectCode: string }) => (
<SelectItem key={project.id} value={project.id}> <SelectItem key={project.publicId || project.id} value={String(project.publicId || project.id)}>
{project.projectCode} - {project.projectName} {project.projectCode} - {project.projectName}
</SelectItem> </SelectItem>
))} ))}
+21 -7
View File
@@ -25,6 +25,16 @@ import { useState, useEffect } from 'react';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
// Helper to extract array data from various API response formats
const extractArrayData = <T,>(value: unknown): T[] => {
if (Array.isArray(value)) return value as T[];
if (value && typeof value === 'object' && 'data' in value) {
const data = (value as { data?: unknown }).data;
if (Array.isArray(data)) return data as T[];
}
return [];
};
// Base Schema // Base Schema
const baseSchema = z.object({ const baseSchema = z.object({
drawingType: z.enum(['CONTRACT', 'SHOP', 'AS_BUILT']), drawingType: z.enum(['CONTRACT', 'SHOP', 'AS_BUILT']),
@@ -79,7 +89,8 @@ export function DrawingUploadForm() {
const router = useRouter(); const router = useRouter();
// Project list // Project list
const { data: projects = [], isLoading: isLoadingProjects } = useProjects(); const { data: projectsData, isLoading: isLoadingProjects } = useProjects();
const projects = extractArrayData<{ id?: number; publicId?: string; projectName: string; projectCode: string }>(projectsData);
// Selected project for category fetching // Selected project for category fetching
const [selectedProjectId, setSelectedProjectId] = useState<number | string | undefined>(undefined); const [selectedProjectId, setSelectedProjectId] = useState<number | string | undefined>(undefined);
@@ -119,9 +130,9 @@ export function DrawingUploadForm() {
} }
// Try to resolve UUID→INT from projects list, or pass UUID directly // Try to resolve UUID→INT from projects list, or pass UUID directly
const project = projects.find( const project = projects.find(
(p: { id: string; uuid?: string }) => p.id === watchedProjectId || p.uuid === watchedProjectId (p: { id?: number; publicId?: string }) => String(p.publicId ?? p.id) === watchedProjectId
) as { id: string; uuid?: string } | undefined; ) as { id?: number; publicId?: string } | undefined;
setSelectedProjectId(project?.id ?? watchedProjectId); setSelectedProjectId(project?.publicId ?? project?.id ?? watchedProjectId);
}, [watchedProjectId, projects]); }, [watchedProjectId, projects]);
const onSubmit = (data: DrawingFormData) => { const onSubmit = (data: DrawingFormData) => {
@@ -181,11 +192,14 @@ export function DrawingUploadForm() {
)} )}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{projects.map((project: { id: string; projectName: string; projectCode: string }) => ( {projects.map((project) => {
<SelectItem key={project.id} value={project.id}> const projectValue = String(project.publicId ?? project.id ?? '');
return (
<SelectItem key={projectValue} value={projectValue}>
{project.projectCode} - {project.projectName} {project.projectCode} - {project.projectName}
</SelectItem> </SelectItem>
))} );
})}
</SelectContent> </SelectContent>
</Select> </Select>
{errors.projectId && <p className="text-sm text-destructive">{errors.projectId.message}</p>} {errors.projectId && <p className="text-sm text-destructive">{errors.projectId.message}</p>}