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: [] };
+40 -38
View File
@@ -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({});
});
});
});
+17 -17
View File
@@ -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;
},
+7 -7
View File
@@ -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;
}
},
};
+4 -8
View File
@@ -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;
},
+11 -11
View File
@@ -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;
}
},
};
+7 -7
View File
@@ -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 [];
},
};
+1 -1
View File
@@ -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';
+6 -10
View File
@@ -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: [] }
}
},
};
+44 -43
View File
@@ -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);
}
},
};
+4 -7
View File
@@ -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,
+6 -6
View File
@@ -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;
},
+5 -9
View File
@@ -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;
},
+5 -9
View File
@@ -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;
}
},
};
+8 -8
View File
@@ -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;
}
},
};
+7 -3
View File
@@ -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) => {
+6 -6
View File
@@ -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;
}
},
};
+7 -7
View File
@@ -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,
+1 -1
View File
@@ -52,7 +52,7 @@ export const useAuthStore = create<AuthState>()(
hasRole: (requiredRole: string) => {
const { user } = get();
return user?.role === requiredRole;
}
},
}),
{
name: 'auth-storage',
+6 -5
View File
@@ -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',
+4 -4
View File
@@ -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',
}
)
);
);
+3 -3
View File
@@ -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));
}
}
+13
View File
@@ -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;
};