260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
+30
-30
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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')}`;
|
||||
|
||||
@@ -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: [] };
|
||||
|
||||
+40
-38
@@ -1,17 +1,20 @@
|
||||
// File: lib/auth.ts
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { z } from "zod";
|
||||
import type { User } from "next-auth";
|
||||
import type { JWT } from "next-auth/jwt";
|
||||
import NextAuth from 'next-auth';
|
||||
import Credentials from 'next-auth/providers/credentials';
|
||||
import { z } from 'zod';
|
||||
import type { User } from 'next-auth';
|
||||
import type { JWT } from 'next-auth/jwt';
|
||||
|
||||
// Schema for input validation
|
||||
const loginSchema = z.object({
|
||||
const _loginSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
const baseUrl = (typeof window === "undefined" ? process.env.INTERNAL_API_URL : null) || process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api";
|
||||
const baseUrl =
|
||||
(typeof window === 'undefined' ? process.env.INTERNAL_API_URL : null) ||
|
||||
process.env.NEXT_PUBLIC_API_URL ||
|
||||
'http://localhost:3001/api';
|
||||
|
||||
// Helper to parse JWT expiry
|
||||
function getJwtExpiry(token: string): number {
|
||||
@@ -44,16 +47,16 @@ function unwrapApiResponse(value: unknown): unknown {
|
||||
let current = value;
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
if (!current || typeof current !== "object") {
|
||||
if (!current || typeof current !== 'object') {
|
||||
return current;
|
||||
}
|
||||
|
||||
const record = current as Record<string, unknown>;
|
||||
if (typeof record.access_token === "string") {
|
||||
if (typeof record.access_token === 'string') {
|
||||
return current;
|
||||
}
|
||||
|
||||
if (!("data" in record)) {
|
||||
if (!('data' in record)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
@@ -64,7 +67,7 @@ function unwrapApiResponse(value: unknown): unknown {
|
||||
}
|
||||
|
||||
function isTokenPayload(value: unknown): value is TokenPayload {
|
||||
return !!value && typeof value === "object" && typeof (value as Record<string, unknown>).access_token === "string";
|
||||
return !!value && typeof value === 'object' && typeof (value as Record<string, unknown>).access_token === 'string';
|
||||
}
|
||||
|
||||
function isLoginPayload(value: unknown): value is LoginPayload {
|
||||
@@ -73,13 +76,13 @@ function isLoginPayload(value: unknown): value is LoginPayload {
|
||||
}
|
||||
|
||||
const user = (value as unknown as { user?: unknown }).user;
|
||||
return !!user && typeof user === "object" && typeof (user as Record<string, unknown>).username === "string";
|
||||
return !!user && typeof user === 'object' && typeof (user as Record<string, unknown>).username === 'string';
|
||||
}
|
||||
|
||||
async function refreshAccessToken(token: JWT) {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/auth/refresh`, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.refreshToken}`,
|
||||
},
|
||||
@@ -94,7 +97,7 @@ async function refreshAccessToken(token: JWT) {
|
||||
const data = unwrapApiResponse(refreshedTokens);
|
||||
|
||||
if (!isTokenPayload(data)) {
|
||||
throw new Error("Invalid refresh response format");
|
||||
throw new Error('Invalid refresh response format');
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -103,12 +106,12 @@ async function refreshAccessToken(token: JWT) {
|
||||
accessTokenExpires: getJwtExpiry(data.access_token),
|
||||
refreshToken: data.refresh_token ?? token.refreshToken,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
// RefreshAccessTokenError - token will be invalidated
|
||||
|
||||
return {
|
||||
...token,
|
||||
error: "RefreshAccessTokenError",
|
||||
error: 'RefreshAccessTokenError',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -121,10 +124,10 @@ export const {
|
||||
} = NextAuth({
|
||||
providers: [
|
||||
Credentials({
|
||||
name: "Credentials",
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
username: { label: "Username", type: "text" },
|
||||
password: { label: "Password", type: "password" },
|
||||
username: { label: 'Username', type: 'text' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
if (!credentials?.username || !credentials?.password) return null;
|
||||
@@ -136,23 +139,23 @@ export const {
|
||||
password: credentials.password as string,
|
||||
};
|
||||
|
||||
console.log(`[AUTH] Attempting login at: ${baseUrl}/auth/login`);
|
||||
console.log(`[AUTH] Current process.env.INTERNAL_API_URL: ${process.env.INTERNAL_API_URL}`);
|
||||
console.log(`[AUTH] Current process.env.NEXT_PUBLIC_API_URL: ${process.env.NEXT_PUBLIC_API_URL}`);
|
||||
// console.log(`[AUTH] Attempting login at: ${baseUrl}/auth/login`); /* TODO: Remove before prod */
|
||||
// console.log(`[AUTH] Current process.env.INTERNAL_API_URL: ${process.env.INTERNAL_API_URL}`); /* TODO: Remove before prod */
|
||||
// console.log(`[AUTH] Current process.env.NEXT_PUBLIC_API_URL: ${process.env.NEXT_PUBLIC_API_URL}`); /* TODO: Remove before prod */
|
||||
|
||||
const res = await fetch(`${baseUrl}/auth/login`, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
cache: 'no-store', // Disable caching for auth requests
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(`[AUTH] Login Failed: status ${res.status}`);
|
||||
const errorBody = await res.text().catch(() => "No error body");
|
||||
console.error(`[AUTH] Error details: ${errorBody}`);
|
||||
// console.error(`[AUTH] Login Failed: status ${res.status}`); /* TODO: Remove before prod */
|
||||
const _errorBody = await res.text().catch(() => 'No error body');
|
||||
// console.error(`[AUTH] Error details: ${errorBody}`); /* TODO: Remove before prod */
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -160,33 +163,32 @@ export const {
|
||||
const backendData = unwrapApiResponse(data);
|
||||
|
||||
if (!isLoginPayload(backendData)) {
|
||||
console.error("[AUTH] Login failed: Invalid response format from backend (missing access_token)");
|
||||
// console.error("[AUTH] Login failed: Invalid response format from backend (missing access_token)"); /* TODO: Remove before prod */
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[AUTH] Login Successful for user: ${backendData.user?.username || 'unknown'}`);
|
||||
// console.log(`[AUTH] Login Successful for user: ${backendData.user?.username || 'unknown'}`); /* TODO: Remove before prod */
|
||||
|
||||
return {
|
||||
id: backendData.user.user_id.toString(),
|
||||
name: `${backendData.user.firstName ?? ""} ${backendData.user.lastName ?? ""}`.trim(),
|
||||
name: `${backendData.user.firstName ?? ''} ${backendData.user.lastName ?? ''}`.trim(),
|
||||
email: backendData.user.email,
|
||||
username: backendData.user.username,
|
||||
role: backendData.user.role || "User",
|
||||
role: backendData.user.role || 'User',
|
||||
organizationId: backendData.user.primaryOrganizationId,
|
||||
accessToken: backendData.access_token,
|
||||
refreshToken: backendData.refresh_token,
|
||||
} as User;
|
||||
|
||||
} catch (error) {
|
||||
console.error("[AUTH] Network/Fetch Error during authorize:", error);
|
||||
} catch (_error) {
|
||||
// console.error("[AUTH] Network/Fetch Error during authorize:", error); /* TODO: Remove before prod */
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
error: "/login",
|
||||
signIn: '/login',
|
||||
error: '/login',
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
@@ -231,9 +233,9 @@ export const {
|
||||
},
|
||||
},
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
strategy: 'jwt',
|
||||
maxAge: 24 * 60 * 60, // 24 hours
|
||||
},
|
||||
secret: process.env.AUTH_SECRET,
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
@@ -135,10 +135,7 @@ describe('correspondenceService', () => {
|
||||
|
||||
const result = await correspondenceService.addReference(1, referenceDto);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith(
|
||||
'/correspondences/1/references',
|
||||
referenceDto
|
||||
);
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/correspondences/1/references', referenceDto);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,6 +90,4 @@ describe('projectService', () => {
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import apiClient from "@/lib/api/client";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { AuditQueryParams } from '@/types/dto/numbering.dto';
|
||||
|
||||
export interface AuditLog {
|
||||
auditId: string;
|
||||
userId?: number | null;
|
||||
user?: {
|
||||
id: number;
|
||||
fullName?: string;
|
||||
username: string;
|
||||
};
|
||||
action: string;
|
||||
severity: string;
|
||||
entityType?: string;
|
||||
entityId?: string;
|
||||
detailsJson?: Record<string, unknown>;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
createdAt: string;
|
||||
auditId: string;
|
||||
userId?: number | null;
|
||||
user?: {
|
||||
id: number;
|
||||
fullName?: string;
|
||||
username: string;
|
||||
};
|
||||
action: string;
|
||||
severity: string;
|
||||
entityType?: string;
|
||||
entityId?: string;
|
||||
detailsJson?: Record<string, unknown>;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type AuditLogQueryParams = AuditQueryParams;
|
||||
|
||||
export const auditLogService = {
|
||||
getLogs: async (params?: AuditLogQueryParams) => {
|
||||
const response = await apiClient.get<{ data: AuditLog[] } | AuditLog[]>("/audit-logs", { params });
|
||||
const response = await apiClient.get<{ data: AuditLog[] } | AuditLog[]>('/audit-logs', { params });
|
||||
// Support both wrapped and unwrapped scenarios
|
||||
return (response.data as { data: AuditLog[] }).data ?? response.data;
|
||||
},
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// File: lib/services/circulation.service.ts
|
||||
import apiClient from "@/lib/api/client";
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
// Import DTO ที่สร้างไว้
|
||||
import { CreateCirculationDto } from "@/types/dto/circulation/create-circulation.dto";
|
||||
import { SearchCirculationDto } from "@/types/dto/circulation/search-circulation.dto";
|
||||
import { UpdateCirculationRoutingDto } from "@/types/dto/circulation/update-circulation-routing.dto";
|
||||
import { CreateCirculationDto } from '@/types/dto/circulation/create-circulation.dto';
|
||||
import { SearchCirculationDto } from '@/types/dto/circulation/search-circulation.dto';
|
||||
import { UpdateCirculationRoutingDto } from '@/types/dto/circulation/update-circulation-routing.dto';
|
||||
|
||||
export const circulationService = {
|
||||
/**
|
||||
@@ -12,7 +12,7 @@ export const circulationService = {
|
||||
*/
|
||||
getAll: async (params?: SearchCirculationDto) => {
|
||||
// GET /circulations
|
||||
const response = await apiClient.get("/circulations", { params });
|
||||
const response = await apiClient.get('/circulations', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@ export const circulationService = {
|
||||
*/
|
||||
create: async (data: CreateCirculationDto) => {
|
||||
// POST /circulations
|
||||
const response = await apiClient.post("/circulations", data);
|
||||
const response = await apiClient.post('/circulations', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -50,5 +50,5 @@ export const circulationService = {
|
||||
delete: async (uuid: string) => {
|
||||
const response = await apiClient.delete(`/circulations/${uuid}`);
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import apiClient from "@/lib/api/client";
|
||||
import {
|
||||
CreateContractDto,
|
||||
UpdateContractDto,
|
||||
SearchContractDto,
|
||||
} from "@/types/dto/contract/contract.dto";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { CreateContractDto, UpdateContractDto, SearchContractDto } from '@/types/dto/contract/contract.dto';
|
||||
|
||||
export const contractService = {
|
||||
/**
|
||||
@@ -11,7 +7,7 @@ export const contractService = {
|
||||
* GET /contracts?projectId=1
|
||||
*/
|
||||
getAll: async (params?: SearchContractDto) => {
|
||||
const response = await apiClient.get("/contracts", { params });
|
||||
const response = await apiClient.get('/contracts', { params });
|
||||
if (response.data && Array.isArray(response.data.data)) {
|
||||
return response.data.data;
|
||||
}
|
||||
@@ -32,7 +28,7 @@ export const contractService = {
|
||||
* POST /contracts
|
||||
*/
|
||||
create: async (data: CreateContractDto) => {
|
||||
const response = await apiClient.post("/contracts", data);
|
||||
const response = await apiClient.post('/contracts', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// File: lib/services/correspondence.service.ts
|
||||
import apiClient from "@/lib/api/client";
|
||||
import { SearchCorrespondenceDto } from "@/types/dto/correspondence/search-correspondence.dto";
|
||||
import { CreateCorrespondenceDto } from "@/types/dto/correspondence/create-correspondence.dto";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { SearchCorrespondenceDto } from '@/types/dto/correspondence/search-correspondence.dto';
|
||||
import { CreateCorrespondenceDto } from '@/types/dto/correspondence/create-correspondence.dto';
|
||||
// Import DTO ใหม่
|
||||
import { SubmitCorrespondenceDto } from "@/types/dto/correspondence/submit-correspondence.dto";
|
||||
import { WorkflowActionDto } from "@/types/dto/correspondence/workflow-action.dto";
|
||||
import { AddReferenceDto, RemoveReferenceDto } from "@/types/dto/correspondence/add-reference.dto";
|
||||
import { SubmitCorrespondenceDto } from '@/types/dto/correspondence/submit-correspondence.dto';
|
||||
import { WorkflowActionDto } from '@/types/dto/correspondence/workflow-action.dto';
|
||||
import { AddReferenceDto, RemoveReferenceDto } from '@/types/dto/correspondence/add-reference.dto';
|
||||
|
||||
export const correspondenceService = {
|
||||
// ... (getAll, getById, create, update, delete เดิมคงไว้) ...
|
||||
|
||||
getAll: async (params?: SearchCorrespondenceDto) => {
|
||||
const response = await apiClient.get("/correspondences", { params });
|
||||
const response = await apiClient.get('/correspondences', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ export const correspondenceService = {
|
||||
},
|
||||
|
||||
create: async (data: CreateCorrespondenceDto) => {
|
||||
const response = await apiClient.post("/correspondences", data);
|
||||
const response = await apiClient.post('/correspondences', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@ export const correspondenceService = {
|
||||
removeReference: async (uuid: string, data: RemoveReferenceDto) => {
|
||||
// ใช้ DELETE method โดยส่ง body ไปด้วย (axios รองรับผ่าน config.data)
|
||||
const response = await apiClient.delete(`/correspondences/${uuid}/references`, {
|
||||
data: data
|
||||
data: data,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
@@ -75,7 +75,7 @@ export const correspondenceService = {
|
||||
* Preview Document Number
|
||||
*/
|
||||
previewNumber: async (data: Partial<CreateCorrespondenceDto>) => {
|
||||
const response = await apiClient.post("/correspondences/preview-number", data);
|
||||
const response = await apiClient.post('/correspondences/preview-number', data);
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import apiClient from "@/lib/api/client";
|
||||
import { DashboardStats, ActivityLog, PendingTask } from "@/types/dashboard";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { DashboardStats, ActivityLog, PendingTask } from '@/types/dashboard';
|
||||
|
||||
export const dashboardService = {
|
||||
getStats: async (): Promise<DashboardStats> => {
|
||||
const response = await apiClient.get("/dashboard/stats");
|
||||
const response = await apiClient.get('/dashboard/stats');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getRecentActivity: async (): Promise<ActivityLog[]> => {
|
||||
try {
|
||||
const response = await apiClient.get("/dashboard/activity");
|
||||
const response = await apiClient.get('/dashboard/activity');
|
||||
// ตรวจสอบว่า response.data เป็น array จริงๆ
|
||||
if (Array.isArray(response.data)) {
|
||||
return response.data;
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
getPendingTasks: async (): Promise<PendingTask[]> => {
|
||||
try {
|
||||
const response = await apiClient.get("/dashboard/pending");
|
||||
const response = await apiClient.get('/dashboard/pending');
|
||||
// Backend คืน { data: [], meta: {} } ต้องดึง data ออกมา
|
||||
if (response.data?.data && Array.isArray(response.data.data)) {
|
||||
return response.data.data;
|
||||
@@ -31,7 +31,7 @@ export const dashboardService = {
|
||||
return response.data;
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import apiClient from "@/lib/api/client";
|
||||
import {
|
||||
NumberingMetrics,
|
||||
ManualOverrideDto,
|
||||
VoidReplaceDto,
|
||||
CancelNumberDto,
|
||||
} from "@/types/dto/numbering.dto";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { NumberingMetrics, ManualOverrideDto, VoidReplaceDto, CancelNumberDto } from '@/types/dto/numbering.dto';
|
||||
|
||||
/** A bulk-import record row */
|
||||
export interface BulkImportRecord {
|
||||
@@ -17,37 +12,37 @@ export interface BulkImportRecord {
|
||||
export const documentNumberingService = {
|
||||
// --- Admin Dashboard Metrics ---
|
||||
getMetrics: async (): Promise<NumberingMetrics> => {
|
||||
const response = await apiClient.get("/admin/document-numbering/metrics");
|
||||
const response = await apiClient.get('/admin/document-numbering/metrics');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// --- Admin Tools ---
|
||||
manualOverride: async (dto: ManualOverrideDto): Promise<void> => {
|
||||
await apiClient.post("/admin/document-numbering/manual-override", dto);
|
||||
await apiClient.post('/admin/document-numbering/manual-override', dto);
|
||||
},
|
||||
|
||||
voidAndReplace: async (dto: VoidReplaceDto): Promise<{ documentNumber: string }> => {
|
||||
const response = await apiClient.post("/admin/document-numbering/void-and-replace", dto);
|
||||
const response = await apiClient.post('/admin/document-numbering/void-and-replace', dto);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
cancelNumber: async (dto: CancelNumberDto): Promise<void> => {
|
||||
await apiClient.post("/admin/document-numbering/cancel", dto);
|
||||
await apiClient.post('/admin/document-numbering/cancel', dto);
|
||||
},
|
||||
|
||||
bulkImport: async (data: FormData | BulkImportRecord[]): Promise<{ imported: number; errors: string[] }> => {
|
||||
const isFormData = data instanceof FormData;
|
||||
const config = isFormData ? { headers: { "Content-Type": "multipart/form-data" } } : {};
|
||||
const response = await apiClient.post("/admin/document-numbering/bulk-import", data, config);
|
||||
const config = isFormData ? { headers: { 'Content-Type': 'multipart/form-data' } } : {};
|
||||
const response = await apiClient.post('/admin/document-numbering/bulk-import', data, config);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// --- Audit Logs ---
|
||||
getAuditLogs: async () => {
|
||||
// NOTE: endpoint might be merged with metrics or separate
|
||||
// Currently controller has getMetrics returning audit logs too.
|
||||
// But if we want separate pagination later:
|
||||
// return apiClient.get("/admin/document-numbering/audit", { params });
|
||||
return [];
|
||||
}
|
||||
// NOTE: endpoint might be merged with metrics or separate
|
||||
// Currently controller has getMetrics returning audit logs too.
|
||||
// But if we want separate pagination later:
|
||||
// return apiClient.get("/admin/document-numbering/audit", { params });
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,4 +18,4 @@ export * from './search.service';
|
||||
export * from './notification.service';
|
||||
export * from './workflow-engine.service';
|
||||
export * from './monitoring.service';
|
||||
export * from './json-schema.service';
|
||||
export * from './json-schema.service';
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// File: lib/services/json-schema.service.ts
|
||||
import apiClient from "@/lib/api/client";
|
||||
import {
|
||||
CreateJsonSchemaDto,
|
||||
UpdateJsonSchemaDto,
|
||||
SearchJsonSchemaDto
|
||||
} from "@/types/dto/json-schema/json-schema.dto";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { CreateJsonSchemaDto, UpdateJsonSchemaDto, SearchJsonSchemaDto } from '@/types/dto/json-schema/json-schema.dto';
|
||||
|
||||
export const jsonSchemaService = {
|
||||
/**
|
||||
@@ -12,7 +8,7 @@ export const jsonSchemaService = {
|
||||
*/
|
||||
getAll: async (params?: SearchJsonSchemaDto) => {
|
||||
// GET /json-schemas
|
||||
const response = await apiClient.get("/json-schemas", { params });
|
||||
const response = await apiClient.get('/json-schemas', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -39,7 +35,7 @@ export const jsonSchemaService = {
|
||||
*/
|
||||
create: async (data: CreateJsonSchemaDto) => {
|
||||
// POST /json-schemas
|
||||
const response = await apiClient.post("/json-schemas", data);
|
||||
const response = await apiClient.post('/json-schemas', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -68,8 +64,8 @@ export const jsonSchemaService = {
|
||||
// POST /json-schemas/validate
|
||||
const response = await apiClient.post(`/json-schemas/validate`, {
|
||||
schemaCode: code,
|
||||
data: data
|
||||
data: data,
|
||||
});
|
||||
return response.data; // { valid: true, errors: [] }
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// File: lib/services/master-data.service.ts
|
||||
import apiClient from "@/lib/api/client";
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
// Import DTOs
|
||||
import { CreateTagDto, UpdateTagDto, SearchTagDto } from "@/types/dto/master/tag.dto";
|
||||
import { CreateDisciplineDto } from "@/types/dto/master/discipline.dto";
|
||||
import { CreateSubTypeDto } from "@/types/dto/master/sub-type.dto";
|
||||
import { SaveNumberFormatDto } from "@/types/dto/master/number-format.dto";
|
||||
import { CreateRfaTypeDto, UpdateRfaTypeDto } from "@/types/dto/master/rfa-type.dto";
|
||||
import { CreateCorrespondenceTypeDto, UpdateCorrespondenceTypeDto } from "@/types/dto/master/correspondence-type.dto";
|
||||
import { Organization } from "@/types/organization";
|
||||
import { CreateTagDto, UpdateTagDto, SearchTagDto } from '@/types/dto/master/tag.dto';
|
||||
import { CreateDisciplineDto } from '@/types/dto/master/discipline.dto';
|
||||
import { CreateSubTypeDto } from '@/types/dto/master/sub-type.dto';
|
||||
import { SaveNumberFormatDto } from '@/types/dto/master/number-format.dto';
|
||||
import { CreateRfaTypeDto, UpdateRfaTypeDto } from '@/types/dto/master/rfa-type.dto';
|
||||
import { CreateCorrespondenceTypeDto, UpdateCorrespondenceTypeDto } from '@/types/dto/master/correspondence-type.dto';
|
||||
import { Organization } from '@/types/organization';
|
||||
import {
|
||||
CreateOrganizationDto,
|
||||
UpdateOrganizationDto,
|
||||
SearchOrganizationDto,
|
||||
} from "@/types/dto/organization/organization.dto";
|
||||
} from '@/types/dto/organization/organization.dto';
|
||||
|
||||
const extractArrayData = <T>(value: unknown): T[] => {
|
||||
let current: unknown = value;
|
||||
@@ -23,7 +23,7 @@ const extractArrayData = <T>(value: unknown): T[] => {
|
||||
return current as T[];
|
||||
}
|
||||
|
||||
if (!current || typeof current !== "object" || !("data" in current)) {
|
||||
if (!current || typeof current !== 'object' || !('data' in current)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -38,14 +38,14 @@ export const masterDataService = {
|
||||
|
||||
/** ดึงรายการ Tags ทั้งหมด (Search & Pagination) */
|
||||
getTags: async (params?: SearchTagDto) => {
|
||||
const response = await apiClient.get("/master/tags", { params });
|
||||
const response = await apiClient.get('/master/tags', { params });
|
||||
// Support both wrapped and unwrapped scenarios
|
||||
return response.data.data || response.data;
|
||||
},
|
||||
|
||||
/** สร้าง Tag ใหม่ */
|
||||
createTag: async (data: CreateTagDto) => {
|
||||
const response = await apiClient.post("/master/tags", data);
|
||||
const response = await apiClient.post('/master/tags', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -65,14 +65,14 @@ export const masterDataService = {
|
||||
|
||||
/** ดึงรายชื่อองค์กรทั้งหมด */
|
||||
getOrganizations: async (params?: SearchOrganizationDto) => {
|
||||
const response = await apiClient.get<Organization[] | { data: Organization[] }>("/organizations", { params });
|
||||
const response = await apiClient.get<Organization[] | { data: Organization[] }>('/organizations', { params });
|
||||
// Support paginated response
|
||||
if (response.data && Array.isArray((response.data as { data: Organization[] }).data)) {
|
||||
return (response.data as { data: Organization[] }).data;
|
||||
return (response.data as { data: Organization[] }).data;
|
||||
}
|
||||
// If response.data itself is an array
|
||||
if (Array.isArray(response.data)) {
|
||||
return response.data;
|
||||
return response.data;
|
||||
}
|
||||
// If we're here, it might be { data: [], total: ... } but data is missing? or empty?
|
||||
// Or it returned the object but data.data check failed (shouldn't happen if it follows schema).
|
||||
@@ -82,8 +82,10 @@ export const masterDataService = {
|
||||
// Fallback: Check if response.data is object?
|
||||
// If it's the paginated object, return the data array if it exists
|
||||
if (response.data && (response.data as { data: Organization[] }).data) {
|
||||
// Maybe it's not an array?
|
||||
return Array.isArray((response.data as { data: Organization[] }).data) ? (response.data as { data: Organization[] }).data : [];
|
||||
// Maybe it's not an array?
|
||||
return Array.isArray((response.data as { data: Organization[] }).data)
|
||||
? (response.data as { data: Organization[] }).data
|
||||
: [];
|
||||
}
|
||||
|
||||
return []; // Return empty array to prevent map errors
|
||||
@@ -91,7 +93,7 @@ export const masterDataService = {
|
||||
|
||||
/** สร้างองค์กรใหม่ */
|
||||
createOrganization: async (data: CreateOrganizationDto) => {
|
||||
const response = await apiClient.post("/organizations", data);
|
||||
const response = await apiClient.post('/organizations', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -107,20 +109,19 @@ export const masterDataService = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
// --- Disciplines Management (Admin / Req 6B) ---
|
||||
|
||||
/** ดึงรายชื่อสาขางาน (มักจะกรองตาม Contract ID) */
|
||||
getDisciplines: async (contractId?: number | string) => {
|
||||
const response = await apiClient.get("/master/disciplines", {
|
||||
params: { contractId }
|
||||
const response = await apiClient.get('/master/disciplines', {
|
||||
params: { contractId },
|
||||
});
|
||||
return extractArrayData(response.data);
|
||||
},
|
||||
|
||||
/** สร้างสาขางานใหม่ */
|
||||
createDiscipline: async (data: CreateDisciplineDto) => {
|
||||
const response = await apiClient.post("/master/disciplines", data);
|
||||
const response = await apiClient.post('/master/disciplines', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -134,15 +135,15 @@ export const masterDataService = {
|
||||
|
||||
/** ดึงรายชื่อประเภทย่อย (กรองตาม Contract และ Type) */
|
||||
getSubTypes: async (contractId?: number | string, typeId?: number) => {
|
||||
const response = await apiClient.get("/master/sub-types", {
|
||||
params: { contractId, correspondenceTypeId: typeId }
|
||||
const response = await apiClient.get('/master/sub-types', {
|
||||
params: { contractId, correspondenceTypeId: typeId },
|
||||
});
|
||||
return extractArrayData(response.data);
|
||||
},
|
||||
|
||||
/** สร้างประเภทย่อยใหม่ */
|
||||
createSubType: async (data: CreateSubTypeDto) => {
|
||||
const response = await apiClient.post("/master/sub-types", data);
|
||||
const response = await apiClient.post('/master/sub-types', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -150,55 +151,55 @@ export const masterDataService = {
|
||||
|
||||
/** ดึงประเภท RFA ทั้งหมด */
|
||||
getRfaTypes: async (contractId?: number | string) => {
|
||||
const response = await apiClient.get("/master/rfa-types", {
|
||||
params: { contractId }
|
||||
const response = await apiClient.get('/master/rfa-types', {
|
||||
params: { contractId },
|
||||
});
|
||||
return extractArrayData(response.data);
|
||||
},
|
||||
|
||||
/** สร้างประเภท RFA ใหม่ */
|
||||
createRfaType: async (data: CreateRfaTypeDto) => {
|
||||
return apiClient.post("/master/rfa-types", data).then(res => res.data);
|
||||
return apiClient.post('/master/rfa-types', data).then((res) => res.data);
|
||||
},
|
||||
|
||||
updateRfaType: async (id: number, data: UpdateRfaTypeDto) => {
|
||||
return apiClient.patch(`/master/rfa-types/${id}`, data).then(res => res.data);
|
||||
return apiClient.patch(`/master/rfa-types/${id}`, data).then((res) => res.data);
|
||||
},
|
||||
|
||||
deleteRfaType: async (id: number) => {
|
||||
return apiClient.delete(`/master/rfa-types/${id}`).then(res => res.data);
|
||||
return apiClient.delete(`/master/rfa-types/${id}`).then((res) => res.data);
|
||||
},
|
||||
|
||||
// --- Document Numbering Format (Admin Config) ---
|
||||
|
||||
// --- Correspondence Types Management ---
|
||||
getCorrespondenceTypes: async () => {
|
||||
const response = await apiClient.get("/master/correspondence-types");
|
||||
const response = await apiClient.get('/master/correspondence-types');
|
||||
return extractArrayData(response.data);
|
||||
},
|
||||
|
||||
createCorrespondenceType: async (data: CreateCorrespondenceTypeDto) => {
|
||||
return apiClient.post("/master/correspondence-types", data).then(res => res.data);
|
||||
return apiClient.post('/master/correspondence-types', data).then((res) => res.data);
|
||||
},
|
||||
|
||||
updateCorrespondenceType: async (id: number, data: UpdateCorrespondenceTypeDto) => {
|
||||
return apiClient.patch(`/master/correspondence-types/${id}`, data).then(res => res.data);
|
||||
return apiClient.patch(`/master/correspondence-types/${id}`, data).then((res) => res.data);
|
||||
},
|
||||
|
||||
deleteCorrespondenceType: async (id: number) => {
|
||||
return apiClient.delete(`/master/correspondence-types/${id}`).then(res => res.data);
|
||||
return apiClient.delete(`/master/correspondence-types/${id}`).then((res) => res.data);
|
||||
},
|
||||
|
||||
/** บันทึกรูปแบบเลขที่เอกสาร */
|
||||
saveNumberFormat: async (data: SaveNumberFormatDto) => {
|
||||
const response = await apiClient.post("/document-numbering/formats", data);
|
||||
const response = await apiClient.post('/document-numbering/formats', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** ดึงรูปแบบเลขที่เอกสารปัจจุบัน (เพื่อมาแก้ไข) */
|
||||
getNumberFormat: async (projectId: number, typeId: number) => {
|
||||
const response = await apiClient.get("/document-numbering/formats", {
|
||||
params: { projectId, correspondenceTypeId: typeId }
|
||||
const response = await apiClient.get('/document-numbering/formats', {
|
||||
params: { projectId, correspondenceTypeId: typeId },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
@@ -206,21 +207,21 @@ export const masterDataService = {
|
||||
// --- Drawing Categories ---
|
||||
|
||||
getContractDrawingCategories: async (projectId?: number | string) => {
|
||||
const response = await apiClient.get("/drawings/contract/categories", {
|
||||
params: { projectId }
|
||||
const response = await apiClient.get('/drawings/contract/categories', {
|
||||
params: { projectId },
|
||||
});
|
||||
return extractArrayData(response.data);
|
||||
},
|
||||
|
||||
getShopMainCategories: async (projectId: number) => {
|
||||
const response = await apiClient.get("/drawings/shop/main-categories", { params: { projectId } });
|
||||
const response = await apiClient.get('/drawings/shop/main-categories', { params: { projectId } });
|
||||
return extractArrayData(response.data);
|
||||
},
|
||||
|
||||
getShopSubCategories: async (projectId: number, mainCategoryId?: number) => {
|
||||
const response = await apiClient.get("/drawings/shop/sub-categories", {
|
||||
params: { projectId, mainCategoryId }
|
||||
const response = await apiClient.get('/drawings/shop/sub-categories', {
|
||||
params: { projectId, mainCategoryId },
|
||||
});
|
||||
return extractArrayData(response.data);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ interface WrappedData {
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
const extractNestedData = <T,>(value: unknown): T => {
|
||||
const extractNestedData = <T>(value: unknown): T => {
|
||||
let current: unknown = value;
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
@@ -25,7 +25,7 @@ const extractNestedData = <T,>(value: unknown): T => {
|
||||
return current as T;
|
||||
};
|
||||
|
||||
const normalizePaginatedResponse = <T,>(value: unknown): PaginatedResponse<T> => {
|
||||
const normalizePaginatedResponse = <T>(value: unknown): PaginatedResponse<T> => {
|
||||
const extracted = extractNestedData<unknown>(value);
|
||||
|
||||
if (!extracted || typeof extracted !== 'object') {
|
||||
@@ -84,15 +84,12 @@ export const migrationService = {
|
||||
return extractNestedData<MigrationReviewQueueItem>(data);
|
||||
},
|
||||
|
||||
getErrors: async (params: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<PaginatedResponse<MigrationErrorItem>> => {
|
||||
getErrors: async (params: { page?: number; limit?: number }): Promise<PaginatedResponse<MigrationErrorItem>> => {
|
||||
const { data } = await api.get('/migration/errors', { params });
|
||||
return normalizePaginatedResponse<MigrationErrorItem>(data);
|
||||
},
|
||||
|
||||
approveQueueItem: async (id: number, payload: any, idempotencyKey: string) => {
|
||||
approveQueueItem: async (id: number, payload: Record<string, unknown>, idempotencyKey: string) => {
|
||||
const { data } = await api.post(`/migration/queue/${id}/approve`, payload, {
|
||||
headers: {
|
||||
'idempotency-key': idempotencyKey,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// File: lib/services/monitoring.service.ts
|
||||
import apiClient from "@/lib/api/client";
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
export interface SetMaintenanceDto {
|
||||
enabled: boolean;
|
||||
@@ -9,19 +9,19 @@ export interface SetMaintenanceDto {
|
||||
export const monitoringService = {
|
||||
/** ตรวจสอบสถานะสุขภาพระบบ (Health Check) */
|
||||
getHealth: async () => {
|
||||
const response = await apiClient.get("/health");
|
||||
const response = await apiClient.get('/health');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** ดึง Metrics การทำงาน (CPU, Memory, Request Count) */
|
||||
getMetrics: async () => {
|
||||
const response = await apiClient.get("/monitoring/metrics");
|
||||
const response = await apiClient.get('/monitoring/metrics');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** เปิด/ปิด Maintenance Mode */
|
||||
setMaintenanceMode: async (data: SetMaintenanceDto) => {
|
||||
const response = await apiClient.post("/monitoring/maintenance", data);
|
||||
const response = await apiClient.post('/monitoring/maintenance', data);
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import apiClient from "@/lib/api/client";
|
||||
import { NotificationResponse } from "@/types/notification";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { NotificationResponse } from '@/types/notification';
|
||||
|
||||
export const notificationService = {
|
||||
getUnread: async (): Promise<NotificationResponse> => {
|
||||
const response = await apiClient.get("/notifications/unread");
|
||||
const response = await apiClient.get('/notifications/unread');
|
||||
// Backend should return { items: [], unreadCount: number }
|
||||
// Or just items and we count on frontend, but typically backend gives count.
|
||||
return response.data;
|
||||
@@ -17,5 +17,5 @@ export const notificationService = {
|
||||
markAllAsRead: async () => {
|
||||
const response = await apiClient.patch(`/notifications/read-all`);
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import apiClient from "@/lib/api/client";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import {
|
||||
CreateOrganizationDto,
|
||||
UpdateOrganizationDto,
|
||||
SearchOrganizationDto,
|
||||
} from "@/types/dto/organization/organization.dto";
|
||||
} from '@/types/dto/organization/organization.dto';
|
||||
|
||||
export const organizationService = {
|
||||
/**
|
||||
@@ -11,7 +11,7 @@ export const organizationService = {
|
||||
* GET /organizations?projectId=1
|
||||
*/
|
||||
getAll: async (params?: SearchOrganizationDto) => {
|
||||
const response = await apiClient.get("/organizations", { params });
|
||||
const response = await apiClient.get('/organizations', { params });
|
||||
// Normalize response if wrapped in data.data or direct data
|
||||
if (response.data && Array.isArray(response.data.data)) {
|
||||
return response.data.data;
|
||||
@@ -33,7 +33,7 @@ export const organizationService = {
|
||||
* POST /organizations
|
||||
*/
|
||||
create: async (data: CreateOrganizationDto) => {
|
||||
const response = await apiClient.post("/organizations", data);
|
||||
const response = await apiClient.post('/organizations', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// File: lib/services/project.service.ts
|
||||
import apiClient from "@/lib/api/client";
|
||||
import {
|
||||
CreateProjectDto,
|
||||
UpdateProjectDto,
|
||||
SearchProjectDto
|
||||
} from "@/types/dto/project/project.dto";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { CreateProjectDto, UpdateProjectDto, SearchProjectDto } from '@/types/dto/project/project.dto';
|
||||
|
||||
export const projectService = {
|
||||
// --- Basic CRUD ---
|
||||
@@ -15,10 +11,10 @@ export const projectService = {
|
||||
*/
|
||||
getAll: async (params?: SearchProjectDto) => {
|
||||
// GET /projects
|
||||
const response = await apiClient.get("/projects", { params });
|
||||
const response = await apiClient.get('/projects', { params });
|
||||
// Handle paginated response
|
||||
if (response.data && Array.isArray(response.data.data)) {
|
||||
return response.data.data;
|
||||
return response.data.data;
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
@@ -31,7 +27,7 @@ export const projectService = {
|
||||
|
||||
/** สร้างโครงการใหม่ (Admin) */
|
||||
create: async (data: CreateProjectDto) => {
|
||||
const response = await apiClient.post("/projects", data);
|
||||
const response = await apiClient.post('/projects', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// File: lib/services/rfa.service.ts
|
||||
import apiClient from "@/lib/api/client";
|
||||
import {
|
||||
CreateRfaDto,
|
||||
UpdateRfaDto,
|
||||
SearchRfaDto
|
||||
} from "@/types/dto/rfa/rfa.dto";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { CreateRfaDto, UpdateRfaDto, SearchRfaDto } from '@/types/dto/rfa/rfa.dto';
|
||||
|
||||
// DTO สำหรับการอนุมัติ (อาจจะย้ายไปไว้ใน folder dto/rfa/ ก็ได้ในอนาคต)
|
||||
export interface WorkflowActionDto {
|
||||
@@ -19,7 +15,7 @@ export const rfaService = {
|
||||
*/
|
||||
getAll: async (params: SearchRfaDto) => {
|
||||
// GET /rfas
|
||||
const response = await apiClient.get("/rfas", { params });
|
||||
const response = await apiClient.get('/rfas', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -37,7 +33,7 @@ export const rfaService = {
|
||||
*/
|
||||
create: async (data: CreateRfaDto) => {
|
||||
// POST /rfas
|
||||
const response = await apiClient.post("/rfas", data);
|
||||
const response = await apiClient.post('/rfas', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -66,5 +62,5 @@ export const rfaService = {
|
||||
// DELETE /rfas/:uuid (ADR-019)
|
||||
const response = await apiClient.delete(`/rfas/${uuid}`);
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// File: lib/services/search.service.ts
|
||||
import apiClient from "@/lib/api/client";
|
||||
import { SearchQueryDto } from "@/types/dto/search/search-query.dto";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { SearchQueryDto } from '@/types/dto/search/search-query.dto';
|
||||
|
||||
export const searchService = {
|
||||
/**
|
||||
@@ -10,8 +10,8 @@ export const searchService = {
|
||||
search: async (query: SearchQueryDto) => {
|
||||
// ส่ง params แบบ flat ตาม DTO
|
||||
// GET /search?q=...&type=...&projectId=...
|
||||
const response = await apiClient.get("/search", {
|
||||
params: query
|
||||
const response = await apiClient.get('/search', {
|
||||
params: query,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
@@ -21,8 +21,8 @@ export const searchService = {
|
||||
* ใช้ search endpoint แต่จำกัดจำนวน
|
||||
*/
|
||||
suggest: async (query: string) => {
|
||||
const response = await apiClient.get("/search", {
|
||||
params: { q: query, limit: 5 }
|
||||
const response = await apiClient.get('/search', {
|
||||
params: { q: query, limit: 5 },
|
||||
});
|
||||
// Assuming backend returns { items: [], ... } or just []
|
||||
return response.data.items || response.data;
|
||||
@@ -32,7 +32,7 @@ export const searchService = {
|
||||
* (Optional) Re-index ข้อมูลใหม่ กรณีข้อมูลไม่ตรง (Admin Only)
|
||||
*/
|
||||
reindex: async (type?: string) => {
|
||||
const response = await apiClient.post("/search/reindex", { type });
|
||||
const response = await apiClient.post('/search/reindex', { type });
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface Session {
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
const extractArrayData = <T,>(value: unknown): T[] => {
|
||||
const extractArrayData = <T>(value: unknown): T[] => {
|
||||
let current: unknown = value;
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
@@ -39,8 +39,12 @@ const transformSession = (session: Session | (Omit<Session, 'id'> & { id: string
|
||||
|
||||
export const sessionService = {
|
||||
getActiveSessions: async (): Promise<Session[]> => {
|
||||
const response = await apiClient.get<Session[] | { data: Session[] } | { data: { data: Session[] } }>('/auth/sessions');
|
||||
return extractArrayData<Session | (Omit<Session, 'id'> & { id: string | number })>(response.data).map(transformSession);
|
||||
const response = await apiClient.get<Session[] | { data: Session[] } | { data: { data: Session[] } }>(
|
||||
'/auth/sessions'
|
||||
);
|
||||
return extractArrayData<Session | (Omit<Session, 'id'> & { id: string | number })>(response.data).map(
|
||||
transformSession
|
||||
);
|
||||
},
|
||||
|
||||
revokeSession: async (sessionId: number) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// File: lib/services/transmittal.service.ts
|
||||
import apiClient from "@/lib/api/client";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import {
|
||||
CreateTransmittalDto,
|
||||
UpdateTransmittalDto,
|
||||
SearchTransmittalDto
|
||||
} from "@/types/dto/transmittal/transmittal.dto";
|
||||
SearchTransmittalDto,
|
||||
} from '@/types/dto/transmittal/transmittal.dto';
|
||||
|
||||
export const transmittalService = {
|
||||
/**
|
||||
@@ -12,7 +12,7 @@ export const transmittalService = {
|
||||
*/
|
||||
getAll: async (params: SearchTransmittalDto) => {
|
||||
// GET /transmittals
|
||||
const response = await apiClient.get("/transmittals", { params });
|
||||
const response = await apiClient.get('/transmittals', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@ export const transmittalService = {
|
||||
*/
|
||||
create: async (data: CreateTransmittalDto) => {
|
||||
// POST /transmittals
|
||||
const response = await apiClient.post("/transmittals", data);
|
||||
const response = await apiClient.post('/transmittals', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -50,5 +50,5 @@ export const transmittalService = {
|
||||
// DELETE /transmittals/:uuid (ADR-019)
|
||||
const response = await apiClient.delete(`/transmittals/${uuid}`);
|
||||
return response.data;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import apiClient from "@/lib/api/client";
|
||||
import { CreateUserDto, UpdateUserDto, SearchUserDto, User, Role } from "@/types/user";
|
||||
import apiClient from '@/lib/api/client';
|
||||
import { CreateUserDto, UpdateUserDto, SearchUserDto, User, Role } from '@/types/user';
|
||||
|
||||
/** Raw API user shape (before transform) */
|
||||
interface RawUser {
|
||||
@@ -9,7 +9,7 @@ interface RawUser {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const extractArrayData = <T,>(value: unknown): T[] => {
|
||||
const extractArrayData = <T>(value: unknown): T[] => {
|
||||
let current: unknown = value;
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
@@ -17,7 +17,7 @@ const extractArrayData = <T,>(value: unknown): T[] => {
|
||||
return current as T[];
|
||||
}
|
||||
|
||||
if (!current || typeof current !== "object" || !("data" in current)) {
|
||||
if (!current || typeof current !== 'object' || !('data' in current)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ type UserListResponse = User[] | { data: User[] | { data: User[] } };
|
||||
|
||||
export const userService = {
|
||||
getAll: async (params?: SearchUserDto) => {
|
||||
const response = await apiClient.get<UserListResponse>("/users", { params });
|
||||
const response = await apiClient.get<UserListResponse>('/users', { params });
|
||||
return extractArrayData<RawUser>(response.data).map(transformUser);
|
||||
},
|
||||
|
||||
getRoles: async (): Promise<Role[]> => {
|
||||
const response = await apiClient.get<{ data: unknown } | unknown>("/users/roles");
|
||||
const response = await apiClient.get<{ data: unknown } | unknown>('/users/roles');
|
||||
return extractArrayData<Role>(response.data);
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ export const userService = {
|
||||
},
|
||||
|
||||
create: async (data: CreateUserDto) => {
|
||||
const response = await apiClient.post<RawUser>("/users", data);
|
||||
const response = await apiClient.post<RawUser>('/users', data);
|
||||
return transformUser(response.data);
|
||||
},
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ interface BackendWorkflowShape {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
const extractArrayData = <T,>(value: unknown): T[] => {
|
||||
const extractArrayData = <T>(value: unknown): T[] => {
|
||||
let current: unknown = value;
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
@@ -52,7 +52,7 @@ const extractArrayData = <T,>(value: unknown): T[] => {
|
||||
return Array.isArray(current) ? (current as T[]) : [];
|
||||
};
|
||||
|
||||
const extractNestedData = <T,>(value: unknown): T => {
|
||||
const extractNestedData = <T>(value: unknown): T => {
|
||||
let current: unknown = value;
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
@@ -106,9 +106,7 @@ const mapWorkflow = (backendObj: BackendWorkflowShape): Workflow => {
|
||||
backendObj.workflow_code ||
|
||||
'',
|
||||
description:
|
||||
backendObj.description ||
|
||||
(typeof backendObj.dsl === 'object' ? backendObj.dsl?.description : undefined) ||
|
||||
'',
|
||||
backendObj.description || (typeof backendObj.dsl === 'object' ? backendObj.dsl?.description : undefined) || '',
|
||||
workflowType: normalizeWorkflowType(backendObj.workflow_code),
|
||||
version: backendObj.version || 1,
|
||||
isActive: backendObj.is_active ?? false,
|
||||
|
||||
@@ -52,7 +52,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
hasRole: (requiredRole: string) => {
|
||||
const { user } = get();
|
||||
return user?.role === requiredRole;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
|
||||
@@ -18,11 +18,12 @@ export const useDraftStore = create<DraftState>()(
|
||||
drafts: {},
|
||||
saveDraft: (key, data) => set((state) => ({ drafts: { ...state.drafts, [key]: data } })),
|
||||
getDraft: (key) => get().drafts[key],
|
||||
clearDraft: (key) => set((state) => {
|
||||
const newDrafts = { ...state.drafts };
|
||||
delete newDrafts[key];
|
||||
return { drafts: newDrafts };
|
||||
}),
|
||||
clearDraft: (key) =>
|
||||
set((state) => {
|
||||
const newDrafts = { ...state.drafts };
|
||||
delete newDrafts[key];
|
||||
return { drafts: newDrafts };
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: 'lcbp3-form-drafts',
|
||||
|
||||
@@ -6,16 +6,16 @@ interface UIState {
|
||||
isSidebarOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
closeSidebar: () => void; // ✅ เพิ่มกลับมา
|
||||
openSidebar: () => void; // ✅ เพิ่มกลับมา
|
||||
openSidebar: () => void; // ✅ เพิ่มกลับมา
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
isSidebarOpen: true,
|
||||
|
||||
|
||||
toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
|
||||
|
||||
|
||||
// ✅ เพิ่ม Implementation กลับมา
|
||||
closeSidebar: () => set({ isSidebarOpen: false }),
|
||||
openSidebar: () => set({ isSidebarOpen: true }),
|
||||
@@ -24,4 +24,4 @@ export const useUIStore = create<UIState>()(
|
||||
name: 'sidebar-state',
|
||||
}
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// File: lib/utils.ts
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
/**
|
||||
* ฟังก์ชันสำหรับรวม ClassNames โดยใช้ clsx และ tailwind-merge
|
||||
@@ -8,4 +8,4 @@ import { twMerge } from "tailwind-merge";
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* UUID Guard Utility
|
||||
* Ensures a string is a valid UUIDv7 (or compatible) before processing.
|
||||
*/
|
||||
export const assertUuid = (value: string): string => {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
if (!uuidRegex.test(value)) {
|
||||
throw new Error(`Invalid UUID format: ${value}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
Reference in New Issue
Block a user