260322:1648 Correct Coresspondence / Doing RFA / Correct CI
CI Pipeline / build (push) Failing after 12m41s
Build and Deploy / deploy (push) Failing after 2m44s

This commit is contained in:
admin
2026-03-22 16:48:12 +07:00
parent e5deedb42e
commit 11984bfa29
683 changed files with 105251 additions and 29068 deletions
+30 -30
View File
@@ -1,60 +1,60 @@
import { User, CreateUserDto, Organization, AuditLog } from "@/types/admin";
import { User, CreateUserDto, Organization, AuditLog } from '@/types/admin';
// Mock Data
const mockUsers: User[] = [
{
userId: 1,
username: "admin",
email: "admin@example.com",
firstName: "System",
lastName: "Admin",
username: 'admin',
email: 'admin@example.com',
firstName: 'System',
lastName: 'Admin',
isActive: true,
roles: [{ roleId: 1, roleName: "ADMIN", description: "Administrator" }],
roles: [{ roleId: 1, roleName: 'ADMIN', description: 'Administrator' }],
},
{
userId: 2,
username: "jdoe",
email: "john.doe@example.com",
firstName: "John",
lastName: "Doe",
username: 'jdoe',
email: 'john.doe@example.com',
firstName: 'John',
lastName: 'Doe',
isActive: true,
roles: [{ roleId: 2, roleName: "USER", description: "Regular User" }],
roles: [{ roleId: 2, roleName: 'USER', description: 'Regular User' }],
},
];
const mockOrgs: Organization[] = [
{
orgId: 1,
orgCode: "PAT",
orgName: "Port Authority of Thailand",
orgNameTh: "การท่าเรือแห่งประเทศไทย",
description: "Owner",
orgCode: 'PAT',
orgName: 'Port Authority of Thailand',
orgNameTh: 'การท่าเรือแห่งประเทศไทย',
description: 'Owner',
},
{
orgId: 2,
orgCode: "CNPC",
orgName: "CNPC Consortium",
description: "Main Contractor",
orgCode: 'CNPC',
orgName: 'CNPC Consortium',
description: 'Main Contractor',
},
];
const mockLogs: AuditLog[] = [
{
auditLogId: 1,
userName: "admin",
action: "CREATE",
entityType: "user",
userName: 'admin',
action: 'CREATE',
entityType: 'user',
description: "Created user 'jdoe'",
ipAddress: "192.168.1.1",
ipAddress: '192.168.1.1',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
},
{
auditLogId: 2,
userName: "jdoe",
action: "UPDATE",
entityType: "rfa",
description: "Updated status of RFA-001 to APPROVED",
ipAddress: "192.168.1.5",
userName: 'jdoe',
action: 'UPDATE',
entityType: 'rfa',
description: 'Updated status of RFA-001 to APPROVED',
ipAddress: '192.168.1.5',
createdAt: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
},
];
@@ -76,8 +76,8 @@ export const adminApi = {
isActive: data.isActive,
roles: data.roles.map((id) => ({
roleId: id,
roleName: id === 1 ? "ADMIN" : "USER",
description: "",
roleName: id === 1 ? 'ADMIN' : 'USER',
description: '',
})),
};
mockUsers.push(newUser);
@@ -89,7 +89,7 @@ export const adminApi = {
return [...mockOrgs];
},
createOrganization: async (data: Omit<Organization, "orgId">): Promise<Organization> => {
createOrganization: async (data: Omit<Organization, 'orgId'>): Promise<Organization> => {
await new Promise((resolve) => setTimeout(resolve, 600));
const newOrg = { ...data, orgId: Math.max(...mockOrgs.map((o) => o.orgId)) + 1 };
mockOrgs.push(newOrg);
+9 -9
View File
@@ -1,15 +1,15 @@
// File: lib/api/client.ts
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosError } from "axios";
import { v4 as uuidv4 } from "uuid";
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosError } from 'axios';
import { v4 as uuidv4 } from 'uuid';
// อ่านค่า Base URL จาก Environment Variable
const baseURL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api";
const baseURL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api';
// สร้าง Axios Instance หลัก
const apiClient: AxiosInstance = axios.create({
baseURL,
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
timeout: 15000, // Timeout 15 วินาที
});
@@ -23,13 +23,13 @@ apiClient.interceptors.request.use(
// 1. Idempotency Key Injection
// ป้องกันการทำรายการซ้ำสำหรับ Method ที่เปลี่ยนแปลงข้อมูล
const method = config.method?.toLowerCase();
if (method && ["post", "put", "delete", "patch"].includes(method)) {
config.headers["Idempotency-Key"] = uuidv4();
if (method && ['post', 'put', 'delete', 'patch'].includes(method)) {
config.headers['Idempotency-Key'] = uuidv4();
}
// 2. Authentication Token Injection
// ดึง Token จาก Zustand persist store (localStorage)
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
try {
const authStorage = localStorage.getItem('auth-storage');
if (authStorage) {
@@ -37,10 +37,10 @@ apiClient.interceptors.request.use(
const token = parsed?.state?.token;
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
config.headers['Authorization'] = `Bearer ${token}`;
}
}
} catch (error) {
} catch (_error) {
// Auth token retrieval failed - request will proceed without token
}
}
+21 -21
View File
@@ -1,4 +1,4 @@
import { DashboardStats, ActivityLog, PendingTask } from "@/types/dashboard";
import { DashboardStats, ActivityLog, PendingTask } from '@/types/dashboard';
export const dashboardApi = {
getStats: async (): Promise<DashboardStats> => {
@@ -18,27 +18,27 @@ export const dashboardApi = {
return [
{
id: 1,
user: { name: "John Doe", initials: "JD" },
action: "Created RFA",
description: "RFA-001: Concrete Pouring Request",
user: { name: 'John Doe', initials: 'JD' },
action: 'Created RFA',
description: 'RFA-001: Concrete Pouring Request',
createdAt: new Date(Date.now() - 1000 * 60 * 30).toISOString(), // 30 mins ago
targetUrl: "/rfas/1",
targetUrl: '/rfas/1',
},
{
id: 2,
user: { name: "Jane Smith", initials: "JS" },
action: "Approved Correspondence",
description: "COR-005: Site Safety Report",
user: { name: 'Jane Smith', initials: 'JS' },
action: 'Approved Correspondence',
description: 'COR-005: Site Safety Report',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(), // 2 hours ago
targetUrl: "/correspondences/5",
targetUrl: '/correspondences/5',
},
{
id: 3,
user: { name: "Mike Johnson", initials: "MJ" },
action: "Uploaded Drawing",
description: "A-101: Ground Floor Plan Rev B",
user: { name: 'Mike Johnson', initials: 'MJ' },
action: 'Uploaded Drawing',
description: 'A-101: Ground Floor Plan Rev B',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 5).toISOString(), // 5 hours ago
targetUrl: "/drawings/1",
targetUrl: '/drawings/1',
},
];
},
@@ -48,19 +48,19 @@ export const dashboardApi = {
return [
{
id: 1,
title: "Review RFA-002",
description: "Approval required for steel reinforcement",
title: 'Review RFA-002',
description: 'Approval required for steel reinforcement',
daysOverdue: 2,
url: "/rfas/2",
priority: "HIGH",
url: '/rfas/2',
priority: 'HIGH',
},
{
id: 2,
title: "Approve Monthly Report",
description: "January 2025 Progress Report",
title: 'Approve Monthly Report',
description: 'January 2025 Progress Report',
daysOverdue: 0,
url: "/correspondences/10",
priority: "MEDIUM",
url: '/correspondences/10',
priority: 'MEDIUM',
},
];
},
+12 -12
View File
@@ -1,24 +1,24 @@
import { Drawing } from "@/types/drawing";
import { Drawing } from '@/types/drawing';
// Mock Data
const mockDrawings: Drawing[] = [
{
drawingId: 1,
drawingNumber: "S-201-A",
title: "Structural Foundation Plan",
discipline: "Structural",
status: "APPROVED",
revision: "A",
drawingNumber: 'S-201-A',
title: 'Structural Foundation Plan',
discipline: 'Structural',
status: 'APPROVED',
revision: 'A',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5).toISOString(),
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
},
{
drawingId: 2,
drawingNumber: "A-101-B",
title: "Architectural Floor Plan - Level 1",
discipline: "Architectural",
status: "IN_REVIEW",
revision: "B",
drawingNumber: 'A-101-B',
title: 'Architectural Floor Plan - Level 1',
discipline: 'Architectural',
status: 'IN_REVIEW',
revision: 'B',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3).toISOString(),
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(),
},
@@ -35,7 +35,7 @@ export const drawingApi = {
return mockDrawings.find((d) => d.drawingId === id);
},
getByContract: async (contractId: number): Promise<{ data: Drawing[] }> => {
getByContract: async (_contractId: number): Promise<{ data: Drawing[] }> => {
await new Promise((resolve) => setTimeout(resolve, 400));
// Mock: return all drawings for any contract
return { data: mockDrawings };
+17 -19
View File
@@ -1,36 +1,36 @@
import { NotificationResponse } from "@/types/notification";
import { NotificationResponse } from '@/types/notification';
// Mock Data
let mockNotifications = [
{
uuid: "019575a0-0001-7000-8000-000000000001",
uuid: '019575a0-0001-7000-8000-000000000001',
notificationId: 1,
title: "RFA Approved",
message: "RFA-001 has been approved by the Project Manager.",
type: "SUCCESS" as const,
title: 'RFA Approved',
message: 'RFA-001 has been approved by the Project Manager.',
type: 'SUCCESS' as const,
isRead: false,
createdAt: new Date(Date.now() - 1000 * 60 * 15).toISOString(), // 15 mins ago
link: "/rfas/1",
link: '/rfas/1',
},
{
uuid: "019575a0-0002-7000-8000-000000000002",
uuid: '019575a0-0002-7000-8000-000000000002',
notificationId: 2,
title: "New Correspondence",
message: "You have received a new correspondence from Contractor A.",
type: "INFO" as const,
title: 'New Correspondence',
message: 'You have received a new correspondence from Contractor A.',
type: 'INFO' as const,
isRead: false,
createdAt: new Date(Date.now() - 1000 * 60 * 60).toISOString(), // 1 hour ago
link: "/correspondences/3",
link: '/correspondences/3',
},
{
uuid: "019575a0-0003-7000-8000-000000000003",
uuid: '019575a0-0003-7000-8000-000000000003',
notificationId: 3,
title: "Drawing Revision Required",
message: "Drawing S-201 requires revision based on recent comments.",
type: "WARNING" as const,
title: 'Drawing Revision Required',
message: 'Drawing S-201 requires revision based on recent comments.',
type: 'WARNING' as const,
isRead: true,
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), // 1 day ago
link: "/drawings/2",
link: '/drawings/2',
},
];
@@ -46,8 +46,6 @@ export const notificationApi = {
markAsRead: async (id: number) => {
await new Promise((resolve) => setTimeout(resolve, 200));
mockNotifications = mockNotifications.map((n) =>
n.notificationId === id ? { ...n, isRead: true } : n
);
mockNotifications = mockNotifications.map((n) => (n.notificationId === id ? { ...n, isRead: true } : n));
},
};
+68 -69
View File
@@ -130,18 +130,18 @@ export const numberingApi = {
* Get all templates
*/
getTemplates: async (): Promise<NumberingTemplate[]> => {
const res = await apiClient.get<any>('/admin/document-numbering/templates');
return res.data.data || res.data;
const res = await apiClient.get<unknown>('/admin/document-numbering/templates');
const data = res.data as { data: NumberingTemplate[] } | NumberingTemplate[];
return Array.isArray(data) ? data : (data as { data: NumberingTemplate[] }).data || [];
},
/**
* Get templates for a specific project
*/
getTemplatesByProject: async (projectId: number): Promise<NumberingTemplate[]> => {
const res = await apiClient.get<any>(
`/admin/document-numbering/templates?projectId=${projectId}`
);
return res.data.data || res.data;
const res = await apiClient.get<unknown>(`/admin/document-numbering/templates?projectId=${projectId}`);
const data = res.data as { data: NumberingTemplate[] } | NumberingTemplate[];
return Array.isArray(data) ? data : (data as { data: NumberingTemplate[] }).data || [];
},
/**
@@ -157,7 +157,7 @@ export const numberingApi = {
*/
saveTemplate: async (dto: SaveTemplateDto): Promise<NumberingTemplate> => {
// Clean the DTO to avoid sending nested objects that might confuse TypeORM or violate constraints
const cleanDto: any = {
const cleanDto: Record<string, unknown> = {
id: dto.id,
projectId: dto.projectId,
correspondenceTypeId: dto.correspondenceTypeId,
@@ -168,11 +168,9 @@ export const numberingApi = {
isActive: dto.isActive ?? 1,
};
const res = await apiClient.post<any>(
'/admin/document-numbering/templates',
cleanDto
);
return res.data.data || res.data;
const res = await apiClient.post<unknown>('/admin/document-numbering/templates', cleanDto);
const data = res.data as Record<string, unknown>;
return (data.id ? data : data.data) as NumberingTemplate;
},
/**
@@ -190,30 +188,33 @@ export const numberingApi = {
* Get audit logs
*/
getAuditLogs: async (limit = 100): Promise<DocumentNumberAudit[]> => {
const res = await apiClient.get<any>(
`/document-numbering/logs/audit?limit=${limit}`
);
return res.data.data || res.data;
const res = await apiClient.get<unknown>(`/document-numbering/logs/audit?limit=${limit}`);
const data = res.data as { data: DocumentNumberAudit[] } | DocumentNumberAudit[];
return Array.isArray(data) ? data : (data as { data: DocumentNumberAudit[] }).data || [];
},
/**
* Get error logs
*/
getErrorLogs: async (limit = 100): Promise<DocumentNumberError[]> => {
const res = await apiClient.get<any>(
`/document-numbering/logs/errors?limit=${limit}`
);
return res.data.data || res.data;
const res = await apiClient.get<unknown>(`/document-numbering/logs/errors?limit=${limit}`);
const data = res.data as { data: DocumentNumberError[] } | DocumentNumberError[];
return Array.isArray(data) ? data : (data as { data: DocumentNumberError[] }).data || [];
},
/**
* Get metrics (audit + errors combined)
*/
getMetrics: async (): Promise<{ audit: DocumentNumberAudit[]; errors: DocumentNumberError[] }> => {
const res = await apiClient.get<any>(
'/admin/document-numbering/metrics'
);
return res.data.data || res.data;
const res = await apiClient.get<unknown>('/admin/document-numbering/metrics');
const data = res.data as {
data?: { audit: DocumentNumberAudit[]; errors: DocumentNumberError[] };
audit?: DocumentNumberAudit[];
errors?: DocumentNumberError[];
};
return 'audit' in data
? (data as { audit: DocumentNumberAudit[]; errors: DocumentNumberError[] })
: data.data || { audit: [], errors: [] };
},
// ----------------------------------------------------------
@@ -224,44 +225,42 @@ export const numberingApi = {
* Manually override/set a counter value
*/
manualOverride: async (dto: ManualOverrideDto): Promise<{ success: boolean; message: string }> => {
const res = await apiClient.post<any>(
'/admin/document-numbering/manual-override',
dto
);
return res.data.data || res.data;
const res = await apiClient.post<unknown>('/admin/document-numbering/manual-override', dto);
const data = res.data as { data?: { success: boolean; message: string }; success?: boolean; message?: string };
return 'success' in data
? (data as { success: boolean; message: string })
: data.data || { success: false, message: '' };
},
/**
* Void a document number and generate replacement
*/
voidAndReplace: async (dto: VoidAndReplaceDto): Promise<{ newNumber: string; auditId: number }> => {
const res = await apiClient.post<any>(
'/admin/document-numbering/void-and-replace',
dto
);
return res.data.data || res.data;
const res = await apiClient.post<unknown>('/admin/document-numbering/void-and-replace', dto);
const data = res.data as { data?: { newNumber: string; auditId: number }; newNumber?: string; auditId?: number };
return 'newNumber' in data
? (data as { newNumber: string; auditId: number })
: data.data || { newNumber: '', auditId: 0 };
},
/**
* Cancel/skip a document number
*/
cancelNumber: async (dto: CancelNumberDto): Promise<{ success: boolean }> => {
const res = await apiClient.post<any>(
'/admin/document-numbering/cancel',
dto
);
return res.data.data || res.data;
const res = await apiClient.post<unknown>('/admin/document-numbering/cancel', dto);
const data = res.data as { data?: { success: boolean }; success?: boolean };
return 'success' in data ? (data as { success: boolean }) : data.data || { success: false };
},
/**
* Bulk import counter values
*/
bulkImport: async (items: BulkImportItem[]): Promise<{ imported: number; errors: string[] }> => {
const res = await apiClient.post<any>(
'/admin/document-numbering/bulk-import',
items
);
return res.data.data || res.data;
const res = await apiClient.post<unknown>('/admin/document-numbering/bulk-import', items);
const data = res.data as { data?: { imported: number; errors: string[] }; imported?: number; errors?: string[] };
return 'imported' in data
? (data as { imported: number; errors: string[] })
: data.data || { imported: 0, errors: [] };
},
/**
@@ -279,11 +278,10 @@ export const numberingApi = {
* Get all counter sequences
*/
getSequences: async (projectId?: number): Promise<NumberSequence[]> => {
const url = projectId
? `/document-numbering/sequences?projectId=${projectId}`
: '/document-numbering/sequences';
const res = await apiClient.get<any>(url);
return res.data.data || res.data;
const url = projectId ? `/document-numbering/sequences?projectId=${projectId}` : '/document-numbering/sequences';
const res = await apiClient.get<unknown>(url);
const data = res.data as { data: NumberSequence[] } | NumberSequence[];
return Array.isArray(data) ? data : (data as { data: NumberSequence[] }).data || [];
},
/**
@@ -298,36 +296,37 @@ export const numberingApi = {
rfaTypeId?: number;
recipientOrganizationId?: number | string;
}): Promise<{ previewNumber: string; nextSequence: number; isDefault: boolean }> => {
const res = await apiClient.post<any>(
'/document-numbering/preview',
ctx
);
// Explicit debug log for frontend developers to see in browser console
console.log("[numberingApi.previewNumber] Raw Response Data:", res.data);
// Preview what a document number would look like (without generating)
const res = await apiClient.post<unknown>('/document-numbering/preview', ctx);
const body = res.data;
console.log("[numberingApi.previewNumber] Full Body:", body);
const body = res.data as Record<string, unknown>;
// Drill down to find the actual data object
let data = body;
let current: Record<string, unknown> = body;
let depth = 0;
while (data && typeof data === 'object' && !data.previewNumber && !data.number && data.data && depth < 3) {
data = data.data;
depth++;
while (
current &&
typeof current === 'object' &&
!current.previewNumber &&
!current.number &&
current.data &&
depth < 3
) {
current = current.data as Record<string, unknown>;
depth++;
}
console.log(`[numberingApi.previewNumber] Unwrapped at depth ${depth}:`, data);
// Final extraction
const previewNumber = data?.previewNumber || data?.number || (typeof data === 'string' ? data : '');
const nextSequence = data?.nextSequence ?? data?.sequence ?? 0;
const isDefault = data?.isDefault === true;
const previewData = current;
const previewNumber =
((typeof current === 'string' ? current : previewData?.previewNumber || previewData?.number) as string) || '';
const nextSequence = (previewData?.nextSequence ?? previewData?.sequence ?? 0) as number;
const isDefault = previewData?.isDefault === true;
return {
previewNumber: previewNumber || JSON.stringify(body), // Fallback to body string if all else fails
nextSequence: nextSequence,
isDefault: isDefault
isDefault: isDefault,
};
},
@@ -337,7 +336,7 @@ export const numberingApi = {
*/
generateTestNumber: async (
_templateId: number,
context: { organizationId: string; disciplineId: string }
_context: { organizationId: string; disciplineId: string }
): Promise<{ number: string }> => {
// Fallback mock for legacy UI - requires proper context for real use
const mockNumber = `TEST-${new Date().getFullYear()}-${String(Math.floor(Math.random() * 9999)).padStart(4, '0')}`;
+9 -9
View File
@@ -1,12 +1,12 @@
import { Workflow, CreateWorkflowDto, ValidationResult } from "@/types/workflow";
import { Workflow, CreateWorkflowDto, ValidationResult } from '@/types/workflow';
// Mock Data
let mockWorkflows: Workflow[] = [
{
workflowId: 1,
workflowName: "Standard RFA Workflow",
description: "Default approval process for RFAs",
workflowType: "RFA",
workflowName: 'Standard RFA Workflow',
description: 'Default approval process for RFAs',
workflowType: 'RFA',
version: 1,
isActive: true,
dslDefinition: `name: Standard RFA Workflow
@@ -23,9 +23,9 @@ steps:
},
{
workflowId: 2,
workflowName: "Correspondence Review",
description: "Incoming correspondence review flow",
workflowType: "CORRESPONDENCE",
workflowName: 'Correspondence Review',
description: 'Incoming correspondence review flow',
workflowType: 'CORRESPONDENCE',
version: 2,
isActive: true,
dslDefinition: `name: Correspondence Review
@@ -66,7 +66,7 @@ export const workflowApi = {
updateWorkflow: async (id: number, data: Partial<CreateWorkflowDto>): Promise<Workflow> => {
await new Promise((resolve) => setTimeout(resolve, 600));
const index = mockWorkflows.findIndex((w) => w.workflowId === 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() };
mockWorkflows[index] = updatedWorkflow;
@@ -76,7 +76,7 @@ export const workflowApi = {
validateDSL: async (dsl: string): Promise<ValidationResult> => {
await new Promise((resolve) => setTimeout(resolve, 400));
// Simple mock validation
if (!dsl.includes("name:") || !dsl.includes("steps:")) {
if (!dsl.includes('name:') || !dsl.includes('steps:')) {
return { valid: false, errors: ["Missing 'name' or 'steps' field"] };
}
return { valid: true, errors: [] };