690328:1703 Fixing Refactor uuid by Kimi #11
CI / CD Pipeline / build (push) Successful in 8m10s
CI / CD Pipeline / deploy (push) Successful in 4m26s

This commit is contained in:
2026-03-28 17:03:12 +07:00
parent 57a3ed2d37
commit 7a9a15560b
15 changed files with 82 additions and 54 deletions
@@ -49,7 +49,7 @@ export default function MigrationReviewQueuePage() {
if (selectedIds.length === items.length) { if (selectedIds.length === items.length) {
setSelectedIds([]); setSelectedIds([]);
} else { } else {
setSelectedIds(items.map((i) => i.id)); setSelectedIds(items.map((i) => i.id).filter((id): id is number => id !== undefined));
} }
}; };
@@ -63,6 +63,7 @@ export default function MigrationReviewQueuePage() {
setSubmitting(true); setSubmitting(true);
const batchItems = items const batchItems = items
.filter((i): i is typeof i & { id: number } => i.id !== undefined)
.filter((i) => selectedIds.includes(i.id)) .filter((i) => selectedIds.includes(i.id))
.map((item) => ({ .map((item) => ({
queueId: item.id, queueId: item.id,
@@ -165,12 +166,12 @@ export default function MigrationReviewQueuePage() {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{items.map((item) => ( {items.map((item) => (
<TableRow key={item.id}> <TableRow key={item.id || item.publicId}>
<TableCell> <TableCell>
<Checkbox <Checkbox
checked={selectedIds.includes(item.id)} checked={item.id !== undefined && selectedIds.includes(item.id)}
onCheckedChange={() => handleToggleSelect(item.id)} onCheckedChange={() => item.id !== undefined && handleToggleSelect(item.id)}
aria-label={`Select item ${item.id}`} aria-label={`Select item ${item.id || item.publicId}`}
/> />
</TableCell> </TableCell>
<TableCell className="font-medium">{item.documentNumber}</TableCell> <TableCell className="font-medium">{item.documentNumber}</TableCell>
@@ -205,7 +206,7 @@ export default function MigrationReviewQueuePage() {
</TableCell> </TableCell>
<TableCell>{format(new Date(item.createdAt), 'dd MMM yyyy, HH:mm')}</TableCell> <TableCell>{format(new Date(item.createdAt), 'dd MMM yyyy, HH:mm')}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Link href={`/admin/migration/review/${item.id}`}> <Link href={`/admin/migration/review/${item.id || item.publicId}`}>
<Button size="sm" variant="ghost"> <Button size="sm" variant="ghost">
<EyeIcon className="h-4 w-4 mr-2" /> Review <EyeIcon className="h-4 w-4 mr-2" /> Review
</Button> </Button>
@@ -125,6 +125,10 @@ export default function MigrationReviewPage() {
}, },
}; };
if (!item?.id) {
toast.error('Invalid item ID');
return;
}
// Mock idempotency key based on timestamp to ensure uniqueness per approval retry // Mock idempotency key based on timestamp to ensure uniqueness per approval retry
const idempotencyKey = `review-${item.id}-${Date.now()}`; const idempotencyKey = `review-${item.id}-${Date.now()}`;
await migrationService.approveQueueItem(item.id, payload, idempotencyKey); await migrationService.approveQueueItem(item.id, payload, idempotencyKey);
@@ -140,7 +144,7 @@ export default function MigrationReviewPage() {
}; };
const onReject = async () => { const onReject = async () => {
if (!item || !confirm('Are you sure you want to REJECT this document? It will not be imported.')) return; if (!item || !item.id || !confirm('Are you sure you want to REJECT this document? It will not be imported.')) return;
try { try {
setSubmitting(true); setSubmitting(true);
@@ -31,7 +31,7 @@ export default function AuditLogsPage() {
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<span className="font-medium text-sm"> <span className="font-medium text-sm">
{log.user?.fullName || log.user?.username || `User #${log.user?.userId || 'System'}`} {log.user?.fullName || log.user?.username || `User #${log.userId || 'System'}`}
</span> </span>
<Badge <Badge
variant={log.severity === 'ERROR' ? 'destructive' : 'outline'} variant={log.severity === 'ERROR' ? 'destructive' : 'outline'}
@@ -10,7 +10,8 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from '@/lib/api/client'; import apiClient from '@/lib/api/client';
interface Role { interface Role {
roleId: number; publicId?: string; // ADR-019: public identifier
roleId?: number; // Internal INT
roleName: string; roleName: string;
permissions?: Permission[]; permissions?: Permission[];
} }
@@ -151,16 +152,17 @@ export function RbacMatrix() {
<div className="text-xs text-muted-foreground">{perm.description}</div> <div className="text-xs text-muted-foreground">{perm.description}</div>
</TableCell> </TableCell>
{roleList.map((role) => { {roleList.map((role) => {
const roleId = role.roleId ?? 0;
// Assume role.permissions is populated // Assume role.permissions is populated
const currentRolePerms = role.permissions?.map((p) => p.permissionId) || []; const currentRolePerms = role.permissions?.map((p) => p.permissionId) || [];
const activePerms = pendingChanges[role.publicId] || currentRolePerms; const activePerms = pendingChanges[roleId] || currentRolePerms;
const isChecked = activePerms.includes(perm.permissionId); const isChecked = activePerms.includes(perm.permissionId);
return ( return (
<TableCell key={`${role.publicId}-${perm.permissionId}`} className="text-center"> <TableCell key={`${roleId}-${perm.permissionId}`} className="text-center">
<Checkbox <Checkbox
checked={isChecked} checked={isChecked}
onCheckedChange={() => handleToggle(role.publicId, perm.permissionId, currentRolePerms)} onCheckedChange={() => handleToggle(roleId, perm.permissionId, currentRolePerms)}
/> />
</TableCell> </TableCell>
); );
+12 -8
View File
@@ -108,7 +108,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
isActive: user.isActive, isActive: user.isActive,
lineId: user.lineId || '', lineId: user.lineId || '',
primaryOrganizationId: user.primaryOrganizationId?.toString() || ALL_ORGANIZATIONS_VALUE, primaryOrganizationId: user.primaryOrganizationId?.toString() || ALL_ORGANIZATIONS_VALUE,
roleIds: user.roles?.map((r: { publicId: string }) => r.publicId) || [], roleIds: user.roles?.map((r: { roleId?: number }) => r.roleId).filter((id): id is number => id !== undefined) || [],
password: '', password: '',
confirmPassword: '', confirmPassword: '',
}); });
@@ -297,19 +297,22 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
<p className="text-sm text-muted-foreground">Loading roles...</p> <p className="text-sm text-muted-foreground">Loading roles...</p>
)} )}
{Array.isArray(roles) && {Array.isArray(roles) &&
roles.map((role: { publicId: string; roleName: string; description?: string }) => ( roles.map((role: { publicId?: string; roleId?: number; roleName: string; description?: string }) => {
<div key={role.publicId} className="flex items-start space-x-2"> const roleId = role.roleId;
if (roleId === undefined) return null;
return (
<div key={role.publicId ?? roleId} className="flex items-start space-x-2">
<Checkbox <Checkbox
id={`role-${role.publicId}`} id={`role-${role.publicId ?? roleId}`}
checked={selectedRoleIds.includes(role.publicId)} checked={selectedRoleIds.includes(roleId)}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
const current = selectedRoleIds; const current = selectedRoleIds;
if (checked) { if (checked) {
setValue('roleIds', [...current, role.publicId]); setValue('roleIds', [...current, roleId]);
} else { } else {
setValue( setValue(
'roleIds', 'roleIds',
current.filter((id) => id !== role.publicId) current.filter((id) => id !== roleId)
); );
} }
}} }}
@@ -324,7 +327,8 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
<p className="text-xs text-muted-foreground">{role.description}</p> <p className="text-xs text-muted-foreground">{role.description}</p>
</div> </div>
</div> </div>
))} );
})}
</div> </div>
</div> </div>
@@ -158,7 +158,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
<SelectContent> <SelectContent>
<SelectItem value="0">Default (All Types)</SelectItem> <SelectItem value="0">Default (All Types)</SelectItem>
{(correspondenceTypes as CorrespondenceType[])?.map((type) => ( {(correspondenceTypes as CorrespondenceType[])?.map((type) => (
<SelectItem key={type.publicId} value={type.publicId.toString()}> <SelectItem key={type.id} value={type.id.toString()}>
{type.typeCode} - {type.typeName} {type.typeCode} - {type.typeName}
</SelectItem> </SelectItem>
))} ))}
@@ -179,7 +179,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
<SelectContent> <SelectContent>
<SelectItem value="0">None</SelectItem> <SelectItem value="0">None</SelectItem>
{(disciplines as Discipline[])?.map((disc) => ( {(disciplines as Discipline[])?.map((disc) => (
<SelectItem key={disc.publicId} value={disc.publicId.toString()}> <SelectItem key={disc.id} value={disc.id.toString()}>
{disc.disciplineCode} {disc.disciplineCode}
</SelectItem> </SelectItem>
))} ))}
+1 -1
View File
@@ -403,7 +403,7 @@ export function RFAForm() {
onValueChange={(val) => { onValueChange={(val) => {
setValue('contractId', val); setValue('contractId', val);
setValue('disciplineId', 0); setValue('disciplineId', 0);
setValue('rfaTypeId', 0); setValue('rfaTypeId', '');
setValue('shopDrawingRevisionIds', []); setValue('shopDrawingRevisionIds', []);
setValue('asBuiltDrawingRevisionIds', []); setValue('asBuiltDrawingRevisionIds', []);
}} }}
+2 -2
View File
@@ -142,7 +142,7 @@ export function useAddTag() {
return useMutation({ return useMutation({
mutationFn: ({ uuid, tagId }: { uuid: string; tagId: number | string }) => mutationFn: ({ uuid, tagId }: { uuid: string; tagId: number | string }) =>
correspondenceService.addTag(uuid, tagId), correspondenceService.addTag(uuid, Number(tagId)),
onSuccess: (_, { uuid }) => { onSuccess: (_, { uuid }) => {
toast.success('Tag added'); toast.success('Tag added');
queryClient.invalidateQueries({ queryKey: [...correspondenceKeys.detail(uuid), 'tags'] }); queryClient.invalidateQueries({ queryKey: [...correspondenceKeys.detail(uuid), 'tags'] });
@@ -160,7 +160,7 @@ export function useRemoveTag() {
return useMutation({ return useMutation({
mutationFn: ({ uuid, tagId }: { uuid: string; tagId: number | string }) => mutationFn: ({ uuid, tagId }: { uuid: string; tagId: number | string }) =>
correspondenceService.removeTag(uuid, tagId), correspondenceService.removeTag(uuid, Number(tagId)),
onSuccess: (_, { uuid }) => { onSuccess: (_, { uuid }) => {
toast.success('Tag removed'); toast.success('Tag removed');
queryClient.invalidateQueries({ queryKey: [...correspondenceKeys.detail(uuid), 'tags'] }); queryClient.invalidateQueries({ queryKey: [...correspondenceKeys.detail(uuid), 'tags'] });
+14 -4
View File
@@ -3,27 +3,30 @@ import { User, CreateUserDto, Organization, AuditLog } from '@/types/admin';
// Mock Data // Mock Data
const mockUsers: User[] = [ const mockUsers: User[] = [
{ {
publicId: 'user-001',
userId: 1, userId: 1,
username: 'admin', username: 'admin',
email: 'admin@example.com', email: 'admin@example.com',
firstName: 'System', firstName: 'System',
lastName: 'Admin', lastName: 'Admin',
isActive: true, isActive: true,
roles: [{ roleId: 1, roleName: 'ADMIN', description: 'Administrator' }], roles: [{ publicId: 'role-001', roleId: 1, roleName: 'ADMIN', description: 'Administrator' }],
}, },
{ {
publicId: 'user-002',
userId: 2, userId: 2,
username: 'jdoe', username: 'jdoe',
email: 'john.doe@example.com', email: 'john.doe@example.com',
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
isActive: true, isActive: true,
roles: [{ roleId: 2, roleName: 'USER', description: 'Regular User' }], roles: [{ publicId: 'role-002', roleId: 2, roleName: 'USER', description: 'Regular User' }],
}, },
]; ];
const mockOrgs: Organization[] = [ const mockOrgs: Organization[] = [
{ {
publicId: 'org-001',
orgId: 1, orgId: 1,
orgCode: 'PAT', orgCode: 'PAT',
orgName: 'Port Authority of Thailand', orgName: 'Port Authority of Thailand',
@@ -31,6 +34,7 @@ const mockOrgs: Organization[] = [
description: 'Owner', description: 'Owner',
}, },
{ {
publicId: 'org-002',
orgId: 2, orgId: 2,
orgCode: 'CNPC', orgCode: 'CNPC',
orgName: 'CNPC Consortium', orgName: 'CNPC Consortium',
@@ -40,6 +44,7 @@ const mockOrgs: Organization[] = [
const mockLogs: AuditLog[] = [ const mockLogs: AuditLog[] = [
{ {
publicId: 'log-001',
auditLogId: 1, auditLogId: 1,
userName: 'admin', userName: 'admin',
action: 'CREATE', action: 'CREATE',
@@ -49,6 +54,7 @@ const mockLogs: AuditLog[] = [
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
}, },
{ {
publicId: 'log-002',
auditLogId: 2, auditLogId: 2,
userName: 'jdoe', userName: 'jdoe',
action: 'UPDATE', action: 'UPDATE',
@@ -67,14 +73,17 @@ export const adminApi = {
createUser: async (data: CreateUserDto): Promise<User> => { createUser: async (data: CreateUserDto): Promise<User> => {
await new Promise((resolve) => setTimeout(resolve, 800)); await new Promise((resolve) => setTimeout(resolve, 800));
const maxId = mockUsers.length > 0 ? Math.max(...mockUsers.map((u) => u.userId ?? 0)) : 0;
const newUser: User = { const newUser: User = {
userId: Math.max(...mockUsers.map((u) => u.userId)) + 1, publicId: `user-${String(maxId + 1).padStart(3, '0')}`,
userId: maxId + 1,
username: data.username, username: data.username,
email: data.email, email: data.email,
firstName: data.firstName, firstName: data.firstName,
lastName: data.lastName, lastName: data.lastName,
isActive: data.isActive, isActive: data.isActive,
roles: data.roles.map((id) => ({ roles: data.roles.map((id) => ({
publicId: `role-${String(id).padStart(3, '0')}`,
roleId: id, roleId: id,
roleName: id === 1 ? 'ADMIN' : 'USER', roleName: id === 1 ? 'ADMIN' : 'USER',
description: '', description: '',
@@ -91,7 +100,8 @@ export const adminApi = {
createOrganization: async (data: Omit<Organization, 'orgId'>): Promise<Organization> => { createOrganization: async (data: Omit<Organization, 'orgId'>): Promise<Organization> => {
await new Promise((resolve) => setTimeout(resolve, 600)); await new Promise((resolve) => setTimeout(resolve, 600));
const newOrg = { ...data, orgId: Math.max(...mockOrgs.map((o) => o.orgId)) + 1 }; const maxId = mockOrgs.length > 0 ? Math.max(...mockOrgs.map((o) => o.orgId ?? 0)) : 0;
const newOrg: Organization = { ...data, publicId: data.publicId || `org-${String(maxId + 1).padStart(3, '0')}`, orgId: maxId + 1 };
mockOrgs.push(newOrg); mockOrgs.push(newOrg);
return newOrg; return newOrg;
}, },
+5 -5
View File
@@ -3,7 +3,7 @@ import { Drawing } from '@/types/drawing';
// Mock Data // Mock Data
const mockDrawings: Drawing[] = [ const mockDrawings: Drawing[] = [
{ {
drawingId: 1, publicId: 'dwg-001',
drawingNumber: 'S-201-A', drawingNumber: 'S-201-A',
title: 'Structural Foundation Plan', title: 'Structural Foundation Plan',
discipline: 'Structural', discipline: 'Structural',
@@ -13,7 +13,7 @@ const mockDrawings: Drawing[] = [
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
}, },
{ {
drawingId: 2, publicId: 'dwg-002',
drawingNumber: 'A-101-B', drawingNumber: 'A-101-B',
title: 'Architectural Floor Plan - Level 1', title: 'Architectural Floor Plan - Level 1',
discipline: 'Architectural', discipline: 'Architectural',
@@ -30,12 +30,12 @@ export const drawingApi = {
return { data: mockDrawings, meta: { total: mockDrawings.length } }; return { data: mockDrawings, meta: { total: mockDrawings.length } };
}, },
getById: async (id: number): Promise<Drawing | undefined> => { getById: async (_id: string): Promise<Drawing | undefined> => {
await new Promise((resolve) => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300));
return mockDrawings.find((d) => d.drawingId === id); return mockDrawings.find((d) => d.publicId === _id);
}, },
getByContract: async (_contractId: number): Promise<{ data: Drawing[] }> => { getByContract: async (_contractId: string): Promise<{ data: Drawing[] }> => {
await new Promise((resolve) => setTimeout(resolve, 400)); await new Promise((resolve) => setTimeout(resolve, 400));
// Mock: return all drawings for any contract // Mock: return all drawings for any contract
return { data: mockDrawings }; return { data: mockDrawings };
+9 -5
View File
@@ -3,6 +3,7 @@ import { Workflow, CreateWorkflowDto, ValidationResult } from '@/types/workflow'
// Mock Data // Mock Data
let mockWorkflows: Workflow[] = [ let mockWorkflows: Workflow[] = [
{ {
publicId: 'wf-001',
workflowId: 1, workflowId: 1,
workflowName: 'Standard RFA Workflow', workflowName: 'Standard RFA Workflow',
description: 'Default approval process for RFAs', description: 'Default approval process for RFAs',
@@ -22,6 +23,7 @@ steps:
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}, },
{ {
publicId: 'wf-002',
workflowId: 2, workflowId: 2,
workflowName: 'Correspondence Review', workflowName: 'Correspondence Review',
description: 'Incoming correspondence review flow', description: 'Incoming correspondence review flow',
@@ -44,15 +46,17 @@ export const workflowApi = {
return [...mockWorkflows]; return [...mockWorkflows];
}, },
getWorkflow: async (id: number): Promise<Workflow | undefined> => { getWorkflow: async (id: string): Promise<Workflow | undefined> => {
await new Promise((resolve) => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300));
return mockWorkflows.find((w) => w.workflowId === id); return mockWorkflows.find((w) => w.publicId === id);
}, },
createWorkflow: async (data: CreateWorkflowDto): Promise<Workflow> => { createWorkflow: async (data: CreateWorkflowDto): Promise<Workflow> => {
await new Promise((resolve) => setTimeout(resolve, 800)); await new Promise((resolve) => setTimeout(resolve, 800));
const maxId = mockWorkflows.length > 0 ? Math.max(...mockWorkflows.map((w) => Number(w.workflowId ?? 0))) : 0;
const newWorkflow: Workflow = { const newWorkflow: Workflow = {
workflowId: Math.max(...mockWorkflows.map((w) => Number(w.workflowId))) + 1, publicId: `wf-${String(maxId + 1).padStart(3, '0')}`,
workflowId: maxId + 1,
...data, ...data,
version: 1, version: 1,
isActive: true, isActive: true,
@@ -63,9 +67,9 @@ export const workflowApi = {
return newWorkflow; return newWorkflow;
}, },
updateWorkflow: async (id: number, data: Partial<CreateWorkflowDto>): Promise<Workflow> => { updateWorkflow: async (id: string, data: Partial<CreateWorkflowDto>): Promise<Workflow> => {
await new Promise((resolve) => setTimeout(resolve, 600)); await new Promise((resolve) => setTimeout(resolve, 600));
const index = mockWorkflows.findIndex((w) => w.workflowId === id); const index = mockWorkflows.findIndex((w) => w.publicId === id);
if (index === -1) throw new Error('Workflow not found'); if (index === -1) throw new Error('Workflow not found');
const updatedWorkflow = { ...mockWorkflows[index], ...data, updatedAt: new Date().toISOString() }; const updatedWorkflow = { ...mockWorkflows[index], ...data, updatedAt: new Date().toISOString() };
@@ -2,6 +2,7 @@ import apiClient from '@/lib/api/client';
import { AuditQueryParams } from '@/types/dto/numbering.dto'; import { AuditQueryParams } from '@/types/dto/numbering.dto';
export interface AuditLog { export interface AuditLog {
publicId?: string; // ADR-019: public identifier
auditId: string; auditId: string;
userId?: number | null; userId?: number | null;
user?: { user?: {
+2 -2
View File
@@ -3,11 +3,11 @@ import { CreateContractDto, UpdateContractDto, SearchContractDto } from '@/types
import { Contract } from '@/types/contract'; import { Contract } from '@/types/contract';
const normalizeContract = (record: Contract): Contract => { const normalizeContract = (record: Contract): Contract => {
const publicId = record.publicId ?? record.id; const publicId = record.publicId;
const project = record.project const project = record.project
? { ? {
...record.project, ...record.project,
publicId: record.project.publicId ?? record.project.id, publicId: record.project.publicId,
} }
: undefined; : undefined;
@@ -99,6 +99,7 @@ const normalizeWorkflowType = (workflowCode?: string): WorkflowType => {
const mapWorkflow = (backendObj: BackendWorkflowShape): Workflow => { const mapWorkflow = (backendObj: BackendWorkflowShape): Workflow => {
if (!backendObj) throw new Error('Workflow not found'); if (!backendObj) throw new Error('Workflow not found');
return { return {
publicId: String(backendObj.id ?? ''),
workflowId: backendObj.id ?? backendObj.workflow_code ?? '', workflowId: backendObj.id ?? backendObj.workflow_code ?? '',
workflowName: workflowName:
(typeof backendObj.dsl === 'object' ? backendObj.dsl?.workflowName : undefined) || (typeof backendObj.dsl === 'object' ? backendObj.dsl?.workflowName : undefined) ||
+2 -1
View File
@@ -1,5 +1,6 @@
export interface Role { export interface Role {
roleId: number; publicId?: string; // ADR-019: public identifier
roleId?: number; // Internal INT
roleName: string; roleName: string;
description: string; description: string;
} }