diff --git a/frontend/app/(admin)/admin/migration/page.tsx b/frontend/app/(admin)/admin/migration/page.tsx index 2c54072..9b7c4d9 100644 --- a/frontend/app/(admin)/admin/migration/page.tsx +++ b/frontend/app/(admin)/admin/migration/page.tsx @@ -49,7 +49,7 @@ export default function MigrationReviewQueuePage() { if (selectedIds.length === items.length) { setSelectedIds([]); } 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); const batchItems = items + .filter((i): i is typeof i & { id: number } => i.id !== undefined) .filter((i) => selectedIds.includes(i.id)) .map((item) => ({ queueId: item.id, @@ -165,12 +166,12 @@ export default function MigrationReviewQueuePage() { {items.map((item) => ( - + handleToggleSelect(item.id)} - aria-label={`Select item ${item.id}`} + checked={item.id !== undefined && selectedIds.includes(item.id)} + onCheckedChange={() => item.id !== undefined && handleToggleSelect(item.id)} + aria-label={`Select item ${item.id || item.publicId}`} /> {item.documentNumber} @@ -205,7 +206,7 @@ export default function MigrationReviewQueuePage() { {format(new Date(item.createdAt), 'dd MMM yyyy, HH:mm')} - + diff --git a/frontend/app/(admin)/admin/migration/review/[id]/page.tsx b/frontend/app/(admin)/admin/migration/review/[id]/page.tsx index 5a5594e..927f0aa 100644 --- a/frontend/app/(admin)/admin/migration/review/[id]/page.tsx +++ b/frontend/app/(admin)/admin/migration/review/[id]/page.tsx @@ -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 const idempotencyKey = `review-${item.id}-${Date.now()}`; await migrationService.approveQueueItem(item.id, payload, idempotencyKey); @@ -140,7 +144,7 @@ export default function MigrationReviewPage() { }; 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 { setSubmitting(true); diff --git a/frontend/app/(admin)/admin/monitoring/audit-logs/page.tsx b/frontend/app/(admin)/admin/monitoring/audit-logs/page.tsx index 1352c1c..84e5654 100644 --- a/frontend/app/(admin)/admin/monitoring/audit-logs/page.tsx +++ b/frontend/app/(admin)/admin/monitoring/audit-logs/page.tsx @@ -31,7 +31,7 @@ export default function AuditLogsPage() {
- {log.user?.fullName || log.user?.username || `User #${log.user?.userId || 'System'}`} + {log.user?.fullName || log.user?.username || `User #${log.userId || 'System'}`} {perm.description}
{roleList.map((role) => { + const roleId = role.roleId ?? 0; // Assume role.permissions is populated 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); return ( - + handleToggle(role.publicId, perm.permissionId, currentRolePerms)} + onCheckedChange={() => handleToggle(roleId, perm.permissionId, currentRolePerms)} /> ); diff --git a/frontend/components/admin/user-dialog.tsx b/frontend/components/admin/user-dialog.tsx index 22aba33..b08798f 100644 --- a/frontend/components/admin/user-dialog.tsx +++ b/frontend/components/admin/user-dialog.tsx @@ -108,7 +108,7 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) { isActive: user.isActive, lineId: user.lineId || '', 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: '', confirmPassword: '', }); @@ -297,23 +297,26 @@ export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {

Loading roles...

)} {Array.isArray(roles) && - roles.map((role: { publicId: string; roleName: string; description?: string }) => ( -
- { - const current = selectedRoleIds; - if (checked) { - setValue('roleIds', [...current, role.publicId]); - } else { - setValue( - 'roleIds', - current.filter((id) => id !== role.publicId) - ); - } - }} - /> + roles.map((role: { publicId?: string; roleId?: number; roleName: string; description?: string }) => { + const roleId = role.roleId; + if (roleId === undefined) return null; + return ( +
+ { + const current = selectedRoleIds; + if (checked) { + setValue('roleIds', [...current, roleId]); + } else { + setValue( + 'roleIds', + current.filter((id) => id !== roleId) + ); + } + }} + />
-
- ))} +
+ ); + })}
diff --git a/frontend/components/numbering/template-tester.tsx b/frontend/components/numbering/template-tester.tsx index 056ff26..e92512d 100644 --- a/frontend/components/numbering/template-tester.tsx +++ b/frontend/components/numbering/template-tester.tsx @@ -158,7 +158,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP Default (All Types) {(correspondenceTypes as CorrespondenceType[])?.map((type) => ( - + {type.typeCode} - {type.typeName} ))} @@ -179,7 +179,7 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP None {(disciplines as Discipline[])?.map((disc) => ( - + {disc.disciplineCode} ))} diff --git a/frontend/components/rfas/form.tsx b/frontend/components/rfas/form.tsx index 95436c0..76b6a91 100644 --- a/frontend/components/rfas/form.tsx +++ b/frontend/components/rfas/form.tsx @@ -403,7 +403,7 @@ export function RFAForm() { onValueChange={(val) => { setValue('contractId', val); setValue('disciplineId', 0); - setValue('rfaTypeId', 0); + setValue('rfaTypeId', ''); setValue('shopDrawingRevisionIds', []); setValue('asBuiltDrawingRevisionIds', []); }} diff --git a/frontend/hooks/use-correspondence.ts b/frontend/hooks/use-correspondence.ts index f6e8d3c..d4b25b3 100644 --- a/frontend/hooks/use-correspondence.ts +++ b/frontend/hooks/use-correspondence.ts @@ -142,7 +142,7 @@ export function useAddTag() { return useMutation({ mutationFn: ({ uuid, tagId }: { uuid: string; tagId: number | string }) => - correspondenceService.addTag(uuid, tagId), + correspondenceService.addTag(uuid, Number(tagId)), onSuccess: (_, { uuid }) => { toast.success('Tag added'); queryClient.invalidateQueries({ queryKey: [...correspondenceKeys.detail(uuid), 'tags'] }); @@ -160,7 +160,7 @@ export function useRemoveTag() { return useMutation({ mutationFn: ({ uuid, tagId }: { uuid: string; tagId: number | string }) => - correspondenceService.removeTag(uuid, tagId), + correspondenceService.removeTag(uuid, Number(tagId)), onSuccess: (_, { uuid }) => { toast.success('Tag removed'); queryClient.invalidateQueries({ queryKey: [...correspondenceKeys.detail(uuid), 'tags'] }); diff --git a/frontend/lib/api/admin.ts b/frontend/lib/api/admin.ts index f09eca3..7d03dbe 100644 --- a/frontend/lib/api/admin.ts +++ b/frontend/lib/api/admin.ts @@ -3,27 +3,30 @@ import { User, CreateUserDto, Organization, AuditLog } from '@/types/admin'; // Mock Data const mockUsers: User[] = [ { + publicId: 'user-001', userId: 1, username: 'admin', email: 'admin@example.com', firstName: 'System', lastName: 'Admin', isActive: true, - roles: [{ roleId: 1, roleName: 'ADMIN', description: 'Administrator' }], + roles: [{ publicId: 'role-001', roleId: 1, roleName: 'ADMIN', description: 'Administrator' }], }, { + publicId: 'user-002', userId: 2, username: 'jdoe', email: 'john.doe@example.com', firstName: 'John', lastName: 'Doe', isActive: true, - roles: [{ roleId: 2, roleName: 'USER', description: 'Regular User' }], + roles: [{ publicId: 'role-002', roleId: 2, roleName: 'USER', description: 'Regular User' }], }, ]; const mockOrgs: Organization[] = [ { + publicId: 'org-001', orgId: 1, orgCode: 'PAT', orgName: 'Port Authority of Thailand', @@ -31,6 +34,7 @@ const mockOrgs: Organization[] = [ description: 'Owner', }, { + publicId: 'org-002', orgId: 2, orgCode: 'CNPC', orgName: 'CNPC Consortium', @@ -40,6 +44,7 @@ const mockOrgs: Organization[] = [ const mockLogs: AuditLog[] = [ { + publicId: 'log-001', auditLogId: 1, userName: 'admin', action: 'CREATE', @@ -49,6 +54,7 @@ const mockLogs: AuditLog[] = [ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), }, { + publicId: 'log-002', auditLogId: 2, userName: 'jdoe', action: 'UPDATE', @@ -67,14 +73,17 @@ export const adminApi = { createUser: async (data: CreateUserDto): Promise => { await new Promise((resolve) => setTimeout(resolve, 800)); + const maxId = mockUsers.length > 0 ? Math.max(...mockUsers.map((u) => u.userId ?? 0)) : 0; 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, email: data.email, firstName: data.firstName, lastName: data.lastName, isActive: data.isActive, roles: data.roles.map((id) => ({ + publicId: `role-${String(id).padStart(3, '0')}`, roleId: id, roleName: id === 1 ? 'ADMIN' : 'USER', description: '', @@ -91,7 +100,8 @@ export const adminApi = { createOrganization: async (data: Omit): Promise => { 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); return newOrg; }, diff --git a/frontend/lib/api/drawings.ts b/frontend/lib/api/drawings.ts index c892f1d..ae8d813 100644 --- a/frontend/lib/api/drawings.ts +++ b/frontend/lib/api/drawings.ts @@ -3,7 +3,7 @@ import { Drawing } from '@/types/drawing'; // Mock Data const mockDrawings: Drawing[] = [ { - drawingId: 1, + publicId: 'dwg-001', drawingNumber: 'S-201-A', title: 'Structural Foundation Plan', discipline: 'Structural', @@ -13,7 +13,7 @@ const mockDrawings: Drawing[] = [ updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), }, { - drawingId: 2, + publicId: 'dwg-002', drawingNumber: 'A-101-B', title: 'Architectural Floor Plan - Level 1', discipline: 'Architectural', @@ -30,12 +30,12 @@ export const drawingApi = { return { data: mockDrawings, meta: { total: mockDrawings.length } }; }, - getById: async (id: number): Promise => { + getById: async (_id: string): Promise => { 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)); // Mock: return all drawings for any contract return { data: mockDrawings }; diff --git a/frontend/lib/api/workflows.ts b/frontend/lib/api/workflows.ts index 801c3bc..4772e53 100644 --- a/frontend/lib/api/workflows.ts +++ b/frontend/lib/api/workflows.ts @@ -3,6 +3,7 @@ import { Workflow, CreateWorkflowDto, ValidationResult } from '@/types/workflow' // Mock Data let mockWorkflows: Workflow[] = [ { + publicId: 'wf-001', workflowId: 1, workflowName: 'Standard RFA Workflow', description: 'Default approval process for RFAs', @@ -22,6 +23,7 @@ steps: updatedAt: new Date().toISOString(), }, { + publicId: 'wf-002', workflowId: 2, workflowName: 'Correspondence Review', description: 'Incoming correspondence review flow', @@ -44,15 +46,17 @@ export const workflowApi = { return [...mockWorkflows]; }, - getWorkflow: async (id: number): Promise => { + getWorkflow: async (id: string): Promise => { 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 => { 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 = { - workflowId: Math.max(...mockWorkflows.map((w) => Number(w.workflowId))) + 1, + publicId: `wf-${String(maxId + 1).padStart(3, '0')}`, + workflowId: maxId + 1, ...data, version: 1, isActive: true, @@ -63,9 +67,9 @@ export const workflowApi = { return newWorkflow; }, - updateWorkflow: async (id: number, data: Partial): Promise => { + updateWorkflow: async (id: string, data: Partial): Promise => { 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'); const updatedWorkflow = { ...mockWorkflows[index], ...data, updatedAt: new Date().toISOString() }; diff --git a/frontend/lib/services/audit-log.service.ts b/frontend/lib/services/audit-log.service.ts index 7e034b8..9fff064 100644 --- a/frontend/lib/services/audit-log.service.ts +++ b/frontend/lib/services/audit-log.service.ts @@ -2,6 +2,7 @@ import apiClient from '@/lib/api/client'; import { AuditQueryParams } from '@/types/dto/numbering.dto'; export interface AuditLog { + publicId?: string; // ADR-019: public identifier auditId: string; userId?: number | null; user?: { diff --git a/frontend/lib/services/contract.service.ts b/frontend/lib/services/contract.service.ts index 9a78cc1..355c759 100644 --- a/frontend/lib/services/contract.service.ts +++ b/frontend/lib/services/contract.service.ts @@ -3,11 +3,11 @@ import { CreateContractDto, UpdateContractDto, SearchContractDto } from '@/types import { Contract } from '@/types/contract'; const normalizeContract = (record: Contract): Contract => { - const publicId = record.publicId ?? record.id; + const publicId = record.publicId; const project = record.project ? { ...record.project, - publicId: record.project.publicId ?? record.project.id, + publicId: record.project.publicId, } : undefined; diff --git a/frontend/lib/services/workflow-engine.service.ts b/frontend/lib/services/workflow-engine.service.ts index 0814b40..3d01e61 100644 --- a/frontend/lib/services/workflow-engine.service.ts +++ b/frontend/lib/services/workflow-engine.service.ts @@ -99,6 +99,7 @@ const normalizeWorkflowType = (workflowCode?: string): WorkflowType => { const mapWorkflow = (backendObj: BackendWorkflowShape): Workflow => { if (!backendObj) throw new Error('Workflow not found'); return { + publicId: String(backendObj.id ?? ''), workflowId: backendObj.id ?? backendObj.workflow_code ?? '', workflowName: (typeof backendObj.dsl === 'object' ? backendObj.dsl?.workflowName : undefined) || diff --git a/frontend/types/user.ts b/frontend/types/user.ts index 8273f38..e376937 100644 --- a/frontend/types/user.ts +++ b/frontend/types/user.ts @@ -1,5 +1,6 @@ export interface Role { - roleId: number; + publicId?: string; // ADR-019: public identifier + roleId?: number; // Internal INT roleName: string; description: string; }