251127:1700 Frontend Start Build

This commit is contained in:
admin
2025-11-27 17:08:49 +07:00
parent 6abb746e08
commit 4f3aa87a93
1795 changed files with 893474 additions and 10 deletions

View File

@@ -0,0 +1,76 @@
// File: lib/api/client.ts
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosError } from "axios";
import { v4 as uuidv4 } from "uuid";
import { getSession } from "next-auth/react";
// อ่านค่า Base URL จาก Environment Variable
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",
},
timeout: 15000, // Timeout 15 วินาที
});
// ---------------------------------------------------------------------------
// Request Interceptors
// ---------------------------------------------------------------------------
apiClient.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
// 1. Idempotency Key Injection
// ป้องกันการทำรายการซ้ำสำหรับ Method ที่เปลี่ยนแปลงข้อมูล
const method = config.method?.toLowerCase();
if (method && ["post", "put", "delete", "patch"].includes(method)) {
config.headers["Idempotency-Key"] = uuidv4();
}
// 2. Authentication Token Injection
// ดึง Session จาก NextAuth (ทำงานเฉพาะฝั่ง Client)
if (typeof window !== "undefined") {
try {
const session = await getSession();
// @ts-ignore: Session type extended in types/next-auth.d.ts
const token = session?.accessToken;
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
}
} catch (error) {
console.warn("Failed to retrieve session token:", error);
}
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// ---------------------------------------------------------------------------
// Response Interceptors
// ---------------------------------------------------------------------------
apiClient.interceptors.response.use(
(response) => {
return response;
},
(error: AxiosError) => {
if (error.response) {
const { status } = error.response;
// กรณี Token หมดอายุ หรือ ไม่มีสิทธิ์
if (status === 401) {
console.error("Unauthorized: Please login again.");
// สามารถเพิ่ม Logic Redirect ไปหน้า Login ได้ถ้าต้องการ
}
}
return Promise.reject(error);
}
);
export default apiClient;

120
frontend/lib/auth.ts Normal file
View File

@@ -0,0 +1,120 @@
// 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";
// Schema สำหรับ Validate ข้อมูลขาเข้าอีกครั้งเพื่อความปลอดภัย
const loginSchema = z.object({
username: z.string().min(1),
password: z.string().min(1),
});
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
providers: [
Credentials({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials) => {
try {
// 1. Validate ข้อมูลที่ส่งมาจากฟอร์ม
const { username, password } = await loginSchema.parseAsync(credentials);
// อ่านค่าจาก ENV หรือใช้ Default (ต้องมั่นใจว่าชี้ไปที่ Port 3001 และมี /api)
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api";
console.log(`Attempting login to: ${baseUrl}/auth/login`);
// 2. เรียก API ไปยัง NestJS Backend
const res = await fetch(`${baseUrl}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
// ถ้า Backend ตอบกลับมาว่าไม่สำเร็จ (เช่น 401, 404, 500)
if (!res.ok) {
const errorMsg = await res.text();
console.error("Login failed:", errorMsg);
return null;
}
// 3. รับข้อมูล JSON จาก Backend
// โครงสร้างที่ Backend ส่งมา: { statusCode: 200, message: "...", data: { access_token: "...", user: {...} } }
const responseJson = await res.json();
// เจาะเข้าไปเอาข้อมูลจริงใน .data
const backendData = responseJson.data;
// ตรวจสอบว่ามี Token หรือไม่
if (!backendData || !backendData.access_token) {
console.error("No access token received in response data");
return null;
}
// 4. Return ข้อมูล User เพื่อส่งต่อไปยัง JWT Callback
// ต้อง Map ชื่อ Field ให้ตรงกับที่ NextAuth คาดหวัง และเก็บ Access Token
return {
// Map user_id จาก DB ให้เป็น id (string) ตามที่ NextAuth ต้องการ
id: backendData.user.user_id.toString(),
// รวมชื่อจริงนามสกุล
name: `${backendData.user.firstName} ${backendData.user.lastName}`,
email: backendData.user.email,
username: backendData.user.username,
// Role (ถ้า Backend ยังไม่ส่ง role มา อาจต้องใส่ Default หรือปรับ Backend เพิ่มเติม)
role: backendData.user.role || "User",
organizationId: backendData.user.primaryOrganizationId,
// เก็บ Token ไว้ใช้งาน
accessToken: backendData.access_token,
} as User;
} catch (error) {
console.error("Auth error:", error);
return null;
}
},
}),
],
pages: {
signIn: "/login", // กำหนดหน้า Login ของเราเอง
error: "/login", // กรณีเกิด Error ให้กลับมาหน้า Login
},
callbacks: {
// 1. JWT Callback: ทำงานเมื่อสร้าง Token หรืออ่าน Token
async jwt({ token, user }) {
// ถ้ามี user เข้ามา (คือตอน Login ครั้งแรก) ให้บันทึกข้อมูลลง Token
if (user) {
token.id = user.id;
token.role = user.role;
token.organizationId = user.organizationId;
token.accessToken = user.accessToken;
}
return token;
},
// 2. Session Callback: ทำงานเมื่อฝั่ง Client เรียก useSession()
async session({ session, token }) {
// ส่งข้อมูลจาก Token ไปให้ Client ใช้งาน
if (token && session.user) {
session.user.id = token.id as string;
session.user.role = token.role as string;
session.user.organizationId = token.organizationId as number;
session.accessToken = token.accessToken as string;
}
return session;
},
},
session: {
strategy: "jwt",
maxAge: 8 * 60 * 60, // 8 ชั่วโมง
},
secret: process.env.AUTH_SECRET,
debug: process.env.NODE_ENV === "development",
});

View File

@@ -0,0 +1,54 @@
// File: lib/services/circulation.service.ts
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";
export const circulationService = {
/**
* ดึงรายการใบเวียนทั้งหมด (รองรับ Search & Filter)
*/
getAll: async (params?: SearchCirculationDto) => {
// GET /circulations
const response = await apiClient.get("/circulations", { params });
return response.data;
},
/**
* ดึงรายละเอียดใบเวียนตาม ID
*/
getById: async (id: string | number) => {
// GET /circulations/:id
const response = await apiClient.get(`/circulations/${id}`);
return response.data;
},
/**
* สร้างใบเวียนใหม่ (Create Circulation)
*/
create: async (data: CreateCirculationDto) => {
// POST /circulations
const response = await apiClient.post("/circulations", data);
return response.data;
},
/**
* อัปเดตสถานะการเวียน (เช่น รับทราบ / ดำเนินการเสร็จสิ้น)
* มักจะใช้ routingId หรือ circulationId ขึ้นอยู่กับการออกแบบ API หลังบ้าน
*/
updateRouting: async (id: string | number, data: UpdateCirculationRoutingDto) => {
// PATCH /circulations/:id/routing (หรือ endpoint ที่ Backend กำหนด)
const response = await apiClient.patch(`/circulations/${id}/routing`, data);
return response.data;
},
/**
* ลบ/ยกเลิกใบเวียน
*/
delete: async (id: string | number) => {
const response = await apiClient.delete(`/circulations/${id}`);
return response.data;
}
};

View File

@@ -0,0 +1,50 @@
// File: lib/services/contract-drawing.service.ts
import apiClient from "@/lib/api/client";
import {
CreateContractDrawingDto,
UpdateContractDrawingDto,
SearchContractDrawingDto
} from "@/types/dto/drawing/contract-drawing.dto";
export const contractDrawingService = {
/**
* ดึงรายการแบบสัญญา (Contract Drawings)
*/
getAll: async (params: SearchContractDrawingDto) => {
// GET /contract-drawings?projectId=1&page=1...
const response = await apiClient.get("/contract-drawings", { params });
return response.data;
},
/**
* ดึงรายละเอียดตาม ID
*/
getById: async (id: string | number) => {
const response = await apiClient.get(`/contract-drawings/${id}`);
return response.data;
},
/**
* สร้างแบบสัญญาใหม่
*/
create: async (data: CreateContractDrawingDto) => {
const response = await apiClient.post("/contract-drawings", data);
return response.data;
},
/**
* แก้ไขข้อมูลแบบสัญญา
*/
update: async (id: string | number, data: UpdateContractDrawingDto) => {
const response = await apiClient.put(`/contract-drawings/${id}`, data);
return response.data;
},
/**
* ลบแบบสัญญา (Soft Delete)
*/
delete: async (id: string | number) => {
const response = await apiClient.delete(`/contract-drawings/${id}`);
return response.data;
}
};

View File

@@ -0,0 +1,74 @@
// 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 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 });
return response.data;
},
getById: async (id: string | number) => {
const response = await apiClient.get(`/correspondences/${id}`);
return response.data;
},
create: async (data: CreateCorrespondenceDto) => {
const response = await apiClient.post("/correspondences", data);
return response.data;
},
update: async (id: string | number, data: any) => {
const response = await apiClient.put(`/correspondences/${id}`, data);
return response.data;
},
delete: async (id: string | number) => {
const response = await apiClient.delete(`/correspondences/${id}`);
return response.data;
},
// --- 🔥 New Methods ---
/**
* ส่งเอกสาร (Submit) เพื่อเริ่ม Workflow
*/
submit: async (id: string | number, data: SubmitCorrespondenceDto) => {
const response = await apiClient.post(`/correspondences/${id}/submit`, data);
return response.data;
},
/**
* ดำเนินการ Workflow (เช่น Approve, Reject) ในขั้นตอนปัจจุบัน
*/
processWorkflow: async (id: string | number, data: WorkflowActionDto) => {
const response = await apiClient.post(`/correspondences/${id}/workflow`, data);
return response.data;
},
/**
* เพิ่มเอกสารอ้างอิง
*/
addReference: async (id: string | number, data: AddReferenceDto) => {
const response = await apiClient.post(`/correspondences/${id}/references`, data);
return response.data;
},
/**
* ลบเอกสารอ้างอิง
*/
removeReference: async (id: string | number, data: RemoveReferenceDto) => {
// ใช้ DELETE method โดยส่ง body ไปด้วย (axios รองรับผ่าน config.data)
const response = await apiClient.delete(`/correspondences/${id}/references`, {
data: data
});
return response.data;
}
};

View File

@@ -0,0 +1,21 @@
// File: lib/services/index.ts
// --- Core Master Data & Projects ---
export * from './project.service';
export * from './master-data.service';
// --- Document Modules ---
export * from './correspondence.service';
export * from './rfa.service';
export * from './contract-drawing.service';
export * from './shop-drawing.service';
export * from './circulation.service';
export * from './transmittal.service';
// --- System & Support Modules (New) ---
export * from './user.service';
export * from './search.service';
export * from './notification.service';
export * from './workflow-engine.service';
export * from './monitoring.service';
export * from './json-schema.service';

View File

@@ -0,0 +1,75 @@
// 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";
export const jsonSchemaService = {
/**
* ดึงรายการ Schema ทั้งหมด (สำหรับ Admin จัดการ Registry)
*/
getAll: async (params?: SearchJsonSchemaDto) => {
// GET /json-schemas
const response = await apiClient.get("/json-schemas", { params });
return response.data;
},
/**
* ดึงรายละเอียด Schema ตาม ID
*/
getById: async (id: number | string) => {
// GET /json-schemas/:id
const response = await apiClient.get(`/json-schemas/${id}`);
return response.data;
},
/**
* ดึง Schema ตาม "Code" (ใช้บ่อยที่สุดสำหรับ Dynamic Form)
*/
getByCode: async (code: string) => {
// GET /json-schemas/code/:code
const response = await apiClient.get(`/json-schemas/code/${code}`);
return response.data;
},
/**
* สร้าง Schema ใหม่ (Admin)
*/
create: async (data: CreateJsonSchemaDto) => {
// POST /json-schemas
const response = await apiClient.post("/json-schemas", data);
return response.data;
},
/**
* อัปเดต Schema (Admin)
*/
update: async (id: number | string, data: UpdateJsonSchemaDto) => {
// PUT /json-schemas/:id
const response = await apiClient.put(`/json-schemas/${id}`, data);
return response.data;
},
/**
* ลบ Schema (Soft Delete)
*/
delete: async (id: number | string) => {
// DELETE /json-schemas/:id
const response = await apiClient.delete(`/json-schemas/${id}`);
return response.data;
},
/**
* (Optional) ตรวจสอบความถูกต้องของข้อมูลกับ Schema ฝั่ง Server
*/
validate: async (code: string, data: any) => {
// POST /json-schemas/validate
const response = await apiClient.post(`/json-schemas/validate`, {
schemaCode: code,
data: data
});
return response.data; // { valid: true, errors: [] }
}
};

View File

@@ -0,0 +1,84 @@
// File: lib/services/master-data.service.ts
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";
export const masterDataService = {
// --- Tags Management ---
/** ดึงรายการ Tags ทั้งหมด (Search & Pagination) */
getTags: async (params?: SearchTagDto) => {
const response = await apiClient.get("/tags", { params });
return response.data;
},
/** สร้าง Tag ใหม่ */
createTag: async (data: CreateTagDto) => {
const response = await apiClient.post("/tags", data);
return response.data;
},
/** แก้ไข Tag */
updateTag: async (id: number | string, data: UpdateTagDto) => {
const response = await apiClient.put(`/tags/${id}`, data);
return response.data;
},
/** ลบ Tag */
deleteTag: async (id: number | string) => {
const response = await apiClient.delete(`/tags/${id}`);
return response.data;
},
// --- Disciplines Management (Admin / Req 6B) ---
/** ดึงรายชื่อสาขางาน (มักจะกรองตาม Contract ID) */
getDisciplines: async (contractId?: number) => {
const response = await apiClient.get("/disciplines", {
params: { contractId }
});
return response.data;
},
/** สร้างสาขางานใหม่ */
createDiscipline: async (data: CreateDisciplineDto) => {
const response = await apiClient.post("/disciplines", data);
return response.data;
},
// --- Sub-Types Management (Admin / Req 6B) ---
/** ดึงรายชื่อประเภทย่อย (กรองตาม Contract และ Type) */
getSubTypes: async (contractId?: number, typeId?: number) => {
const response = await apiClient.get("/sub-types", {
params: { contractId, correspondenceTypeId: typeId }
});
return response.data;
},
/** สร้างประเภทย่อยใหม่ */
createSubType: async (data: CreateSubTypeDto) => {
const response = await apiClient.post("/sub-types", data);
return response.data;
},
// --- Document Numbering Format (Admin Config) ---
/** บันทึกรูปแบบเลขที่เอกสาร */
saveNumberFormat: async (data: SaveNumberFormatDto) => {
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 }
});
return response.data;
}
};

View File

@@ -0,0 +1,27 @@
// File: lib/services/monitoring.service.ts
import apiClient from "@/lib/api/client";
export interface SetMaintenanceDto {
enabled: boolean;
message?: string; // ข้อความที่จะแสดงหน้าเว็บตอนปิดปรับปรุง
}
export const monitoringService = {
/** ตรวจสอบสถานะสุขภาพระบบ (Health Check) */
getHealth: async () => {
const response = await apiClient.get("/health");
return response.data;
},
/** ดึง Metrics การทำงาน (CPU, Memory, Request Count) */
getMetrics: async () => {
const response = await apiClient.get("/monitoring/metrics");
return response.data;
},
/** เปิด/ปิด Maintenance Mode */
setMaintenanceMode: async (data: SetMaintenanceDto) => {
const response = await apiClient.post("/monitoring/maintenance", data);
return response.data;
}
};

View File

@@ -0,0 +1,48 @@
// File: lib/services/notification.service.ts
import apiClient from "@/lib/api/client";
import {
SearchNotificationDto,
CreateNotificationDto
} from "@/types/dto/notification/notification.dto";
export const notificationService = {
/** * ดึงรายการแจ้งเตือนของผู้ใช้ปัจจุบัน
* GET /notifications
*/
getMyNotifications: async (params?: SearchNotificationDto) => {
const response = await apiClient.get("/notifications", { params });
return response.data;
},
/** * สร้างการแจ้งเตือนใหม่ (มักใช้โดย System หรือ Admin)
* POST /notifications
*/
create: async (data: CreateNotificationDto) => {
const response = await apiClient.post("/notifications", data);
return response.data;
},
/** * อ่านแจ้งเตือน (Mark as Read)
* PATCH /notifications/:id/read
*/
markAsRead: async (id: number | string) => {
const response = await apiClient.patch(`/notifications/${id}/read`);
return response.data;
},
/** * อ่านทั้งหมด (Mark All as Read)
* PATCH /notifications/read-all
*/
markAllAsRead: async () => {
const response = await apiClient.patch("/notifications/read-all");
return response.data;
},
/** * ลบการแจ้งเตือน
* DELETE /notifications/:id
*/
delete: async (id: number | string) => {
const response = await apiClient.delete(`/notifications/${id}`);
return response.data;
}
};

View File

@@ -0,0 +1,63 @@
// File: lib/services/project.service.ts
import apiClient from "@/lib/api/client";
import {
CreateProjectDto,
UpdateProjectDto,
SearchProjectDto
} from "@/types/dto/project/project.dto";
export const projectService = {
// --- Basic CRUD ---
/**
* ดึงรายการโครงการทั้งหมด (รองรับ Search & Pagination)
* (เดิมคือ getAllProjects แต่ปรับให้รับ params ได้)
*/
getAll: async (params?: SearchProjectDto) => {
// GET /projects
const response = await apiClient.get("/projects", { params });
return response.data;
},
/** ดึงรายละเอียดโครงการตาม ID */
getById: async (id: string | number) => {
const response = await apiClient.get(`/projects/${id}`);
return response.data;
},
/** สร้างโครงการใหม่ (Admin) */
create: async (data: CreateProjectDto) => {
const response = await apiClient.post("/projects", data);
return response.data;
},
/** แก้ไขโครงการ */
update: async (id: string | number, data: UpdateProjectDto) => {
const response = await apiClient.put(`/projects/${id}`, data);
return response.data;
},
/** ลบโครงการ (Soft Delete) */
delete: async (id: string | number) => {
const response = await apiClient.delete(`/projects/${id}`);
return response.data;
},
// --- Related Data / Dropdown Helpers ---
/** * ดึงรายชื่อองค์กรในโครงการ (สำหรับ Dropdown 'To/From')
* GET /projects/:id/organizations
*/
getOrganizations: async (projectId: string | number) => {
const response = await apiClient.get(`/projects/${projectId}/organizations`);
return response.data;
},
/** * ดึงรายชื่อสัญญาในโครงการ
* GET /projects/:id/contracts
*/
getContracts: async (projectId: string | number) => {
const response = await apiClient.get(`/projects/${projectId}/contracts`);
return response.data;
}
};

View File

@@ -0,0 +1,69 @@
// File: lib/services/rfa.service.ts
import apiClient from "@/lib/api/client";
import {
CreateRfaDto,
UpdateRfaDto,
SearchRfaDto
} from "@/types/dto/rfa/rfa.dto";
// DTO สำหรับการอนุมัติ (อาจจะย้ายไปไว้ใน folder dto/rfa/ ก็ได้ในอนาคต)
export interface WorkflowActionDto {
action: 'APPROVE' | 'REJECT' | 'COMMENT' | 'ACKNOWLEDGE';
comments?: string;
stepNumber?: number; // อาจจะไม่จำเป็นถ้า Backend เช็ค state ปัจจุบันได้เอง
}
export const rfaService = {
/**
* ดึงรายการ RFA ทั้งหมด (รองรับ Search & Filter)
*/
getAll: async (params: SearchRfaDto) => {
// GET /rfas
const response = await apiClient.get("/rfas", { params });
return response.data;
},
/**
* ดึงรายละเอียด RFA และประวัติ Workflow
*/
getById: async (id: string | number) => {
// GET /rfas/:id
const response = await apiClient.get(`/rfas/${id}`);
return response.data;
},
/**
* สร้าง RFA ใหม่
*/
create: async (data: CreateRfaDto) => {
// POST /rfas
const response = await apiClient.post("/rfas", data);
return response.data;
},
/**
* แก้ไข RFA (เฉพาะสถานะ Draft)
*/
update: async (id: string | number, data: UpdateRfaDto) => {
// PUT /rfas/:id
const response = await apiClient.put(`/rfas/${id}`, data);
return response.data;
},
/**
* ดำเนินการ Workflow (อนุมัติ / ตีกลับ / ส่งต่อ)
*/
processWorkflow: async (id: string | number, actionData: WorkflowActionDto) => {
// POST /rfas/:id/workflow
const response = await apiClient.post(`/rfas/${id}/workflow`, actionData);
return response.data;
},
/**
* (Optional) ลบ RFA (Soft Delete)
*/
delete: async (id: string | number) => {
const response = await apiClient.delete(`/rfas/${id}`);
return response.data;
}
};

View File

@@ -0,0 +1,26 @@
// File: lib/services/search.service.ts
import apiClient from "@/lib/api/client";
import { SearchQueryDto } from "@/types/dto/search/search-query.dto";
export const searchService = {
/**
* ค้นหาเอกสารทั้งระบบ (Full-text Search)
* ใช้ Elasticsearch ผ่าน Backend
*/
search: async (query: SearchQueryDto) => {
// ส่ง params แบบ flat ตาม DTO
// GET /search?q=...&type=...&projectId=...
const response = await apiClient.get("/search", {
params: query
});
return response.data;
},
/**
* (Optional) Re-index ข้อมูลใหม่ กรณีข้อมูลไม่ตรง (Admin Only)
*/
reindex: async (type?: string) => {
const response = await apiClient.post("/search/reindex", { type });
return response.data;
}
};

View File

@@ -0,0 +1,41 @@
// File: lib/services/shop-drawing.service.ts
import apiClient from "@/lib/api/client";
import {
CreateShopDrawingDto,
CreateShopDrawingRevisionDto,
SearchShopDrawingDto
} from "@/types/dto/drawing/shop-drawing.dto";
export const shopDrawingService = {
/**
* ดึงรายการแบบก่อสร้าง (Shop Drawings)
*/
getAll: async (params: SearchShopDrawingDto) => {
const response = await apiClient.get("/shop-drawings", { params });
return response.data;
},
/**
* ดึงรายละเอียดตาม ID (ควรได้ Revision History มาด้วย)
*/
getById: async (id: string | number) => {
const response = await apiClient.get(`/shop-drawings/${id}`);
return response.data;
},
/**
* สร้าง Shop Drawing ใหม่ (พร้อม Revision 0)
*/
create: async (data: CreateShopDrawingDto) => {
const response = await apiClient.post("/shop-drawings", data);
return response.data;
},
/**
* สร้าง Revision ใหม่สำหรับ Shop Drawing เดิม
*/
createRevision: async (id: string | number, data: CreateShopDrawingRevisionDto) => {
const response = await apiClient.post(`/shop-drawings/${id}/revisions`, data);
return response.data;
}
};

View File

@@ -0,0 +1,54 @@
// File: lib/services/transmittal.service.ts
import apiClient from "@/lib/api/client";
import {
CreateTransmittalDto,
UpdateTransmittalDto,
SearchTransmittalDto
} from "@/types/dto/transmittal/transmittal.dto";
export const transmittalService = {
/**
* ดึงรายการ Transmittal ทั้งหมด (รองรับ Search & Filter)
*/
getAll: async (params: SearchTransmittalDto) => {
// GET /transmittals
const response = await apiClient.get("/transmittals", { params });
return response.data;
},
/**
* ดึงรายละเอียด Transmittal ตาม ID
*/
getById: async (id: string | number) => {
// GET /transmittals/:id
const response = await apiClient.get(`/transmittals/${id}`);
return response.data;
},
/**
* สร้างเอกสารนำส่งใหม่
*/
create: async (data: CreateTransmittalDto) => {
// POST /transmittals
const response = await apiClient.post("/transmittals", data);
return response.data;
},
/**
* แก้ไขข้อมูล Transmittal (เฉพาะ Draft)
*/
update: async (id: string | number, data: UpdateTransmittalDto) => {
// PUT /transmittals/:id
const response = await apiClient.put(`/transmittals/${id}`, data);
return response.data;
},
/**
* ลบเอกสาร (Soft Delete)
*/
delete: async (id: string | number) => {
// DELETE /transmittals/:id
const response = await apiClient.delete(`/transmittals/${id}`);
return response.data;
}
};

View File

@@ -0,0 +1,57 @@
// File: lib/services/user.service.ts
import apiClient from "@/lib/api/client";
import {
CreateUserDto,
UpdateUserDto,
AssignRoleDto,
UpdatePreferenceDto
} from "@/types/dto/user/user.dto";
export const userService = {
/** ดึงรายชื่อผู้ใช้ทั้งหมด (Admin) */
getAll: async (params?: any) => {
const response = await apiClient.get("/users", { params });
return response.data;
},
/** ดึงข้อมูลผู้ใช้ตาม ID */
getById: async (id: number | string) => {
const response = await apiClient.get(`/users/${id}`);
return response.data;
},
/** สร้างผู้ใช้ใหม่ (Admin) */
create: async (data: CreateUserDto) => {
const response = await apiClient.post("/users", data);
return response.data;
},
/** แก้ไขข้อมูลผู้ใช้ */
update: async (id: number | string, data: UpdateUserDto) => {
const response = await apiClient.put(`/users/${id}`, data);
return response.data;
},
/** แก้ไขการตั้งค่าส่วนตัว (Preferences) */
updatePreferences: async (id: number | string, data: UpdatePreferenceDto) => {
const response = await apiClient.put(`/users/${id}/preferences`, data);
return response.data;
},
/** * กำหนด Role ให้ผู้ใช้ (Admin)
* หมายเหตุ: Backend DTO มี userId ใน body ด้วย แต่ API อาจจะรับ userId ใน param
* ขึ้นอยู่กับการ Implement ของ Controller (ในที่นี้ส่งไปทั้งคู่เพื่อความชัวร์)
*/
assignRole: async (userId: number | string, data: Omit<AssignRoleDto, 'userId'>) => {
// รวม userId เข้าไปใน body เพื่อให้ตรงกับ DTO Validation ฝั่ง Backend
const payload: AssignRoleDto = { userId: Number(userId), ...data };
const response = await apiClient.post(`/users/${userId}/roles`, payload);
return response.data;
},
/** ลบผู้ใช้ (Soft Delete) */
delete: async (id: number | string) => {
const response = await apiClient.delete(`/users/${id}`);
return response.data;
}
};

View File

@@ -0,0 +1,77 @@
// File: lib/services/workflow-engine.service.ts
import apiClient from "@/lib/api/client";
import {
CreateWorkflowDefinitionDto,
UpdateWorkflowDefinitionDto,
EvaluateWorkflowDto,
GetAvailableActionsDto
} from "@/types/dto/workflow-engine/workflow-engine.dto";
export const workflowEngineService = {
// --- Engine Execution (Low-Level) ---
/**
* ตรวจสอบ Action ที่ทำได้ ณ สถานะหนึ่งๆ (ถาม Engine)
* POST /workflow-engine/available-actions
*/
getAvailableActions: async (data: GetAvailableActionsDto) => {
const response = await apiClient.post("/workflow-engine/available-actions", data);
return response.data; // string[] e.g. ['APPROVE', 'REJECT']
},
/**
* ประมวลผล Transition (ถาม Engine ว่าถ้าทำ Action นี้แล้วจะไป State ไหน)
* POST /workflow-engine/evaluate
*/
evaluate: async (data: EvaluateWorkflowDto) => {
const response = await apiClient.post("/workflow-engine/evaluate", data);
return response.data; // { nextState: '...', events: [...] }
},
// --- Definition Management (Admin / Workflow Editor) ---
/**
* ดึง Workflow Definition ทั้งหมด
* GET /workflow-engine/definitions
*/
getDefinitions: async () => {
const response = await apiClient.get("/workflow-engine/definitions");
return response.data;
},
/**
* ดึง Workflow Definition ตาม ID
* GET /workflow-engine/definitions/:id
*/
getDefinitionById: async (id: string | number) => {
const response = await apiClient.get(`/workflow-engine/definitions/${id}`);
return response.data;
},
/**
* สร้าง Workflow Definition ใหม่
* POST /workflow-engine/definitions
*/
createDefinition: async (data: CreateWorkflowDefinitionDto) => {
const response = await apiClient.post("/workflow-engine/definitions", data);
return response.data;
},
/**
* อัปเดต Workflow Definition
* PATCH /workflow-engine/definitions/:id
*/
updateDefinition: async (id: string | number, data: UpdateWorkflowDefinitionDto) => {
const response = await apiClient.patch(`/workflow-engine/definitions/${id}`, data);
return response.data;
},
/**
* ลบ Workflow Definition
* DELETE /workflow-engine/definitions/:id
*/
deleteDefinition: async (id: string | number) => {
const response = await apiClient.delete(`/workflow-engine/definitions/${id}`);
return response.data;
}
};

View File

@@ -0,0 +1,29 @@
// File: lib/stores/draft-store.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface DraftState {
drafts: Record<string, any>;
saveDraft: (key: string, data: any) => void;
getDraft: (key: string) => any;
clearDraft: (key: string) => void;
}
export const useDraftStore = create<DraftState>()(
persist(
(set, get) => ({
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 };
}),
}),
{
name: 'lcbp3-form-drafts',
storage: createJSONStorage(() => localStorage),
}
)
);

View File

@@ -0,0 +1,27 @@
// File: lib/stores/ui-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface UIState {
isSidebarOpen: boolean;
toggleSidebar: () => void;
closeSidebar: () => 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 }),
}),
{
name: 'sidebar-state',
}
)
);

11
frontend/lib/utils.ts Normal file
View File

@@ -0,0 +1,11 @@
// File: lib/utils.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* ฟังก์ชันสำหรับรวม ClassNames โดยใช้ clsx และ tailwind-merge
* ช่วยแก้ปัญหา class conflict ของ Tailwind
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}