251208:0010 Backend & Frontend Debug
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
2025-12-08 00:10:37 +07:00
parent 32d820ea6b
commit dcd126d704
99 changed files with 2775 additions and 1480 deletions

View File

@@ -1,85 +0,0 @@
import { Correspondence, CreateCorrespondenceDto } from "@/types/correspondence";
// Mock Data
const mockCorrespondences: Correspondence[] = [
{
correspondence_id: 1,
document_number: "LCBP3-COR-001",
subject: "Submission of Monthly Report - Jan 2025",
description: "Please find attached the monthly progress report.",
status: "PENDING",
importance: "NORMAL",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
from_organization_id: 1,
to_organization_id: 2,
document_type_id: 1,
from_organization: { id: 1, org_name: "Contractor A", org_code: "CON-A" },
to_organization: { id: 2, org_name: "Owner", org_code: "OWN" },
},
{
correspondence_id: 2,
document_number: "LCBP3-COR-002",
subject: "Request for Information regarding Foundation",
description: "Clarification needed on drawing A-101.",
status: "IN_REVIEW",
importance: "HIGH",
created_at: new Date(Date.now() - 86400000).toISOString(),
updated_at: new Date(Date.now() - 86400000).toISOString(),
from_organization_id: 2,
to_organization_id: 1,
document_type_id: 1,
from_organization: { id: 2, org_name: "Owner", org_code: "OWN" },
to_organization: { id: 1, org_name: "Contractor A", org_code: "CON-A" },
},
];
export const correspondenceApi = {
getAll: async (params?: { page?: number; status?: string; search?: string }) => {
// Simulate API delay
await new Promise((resolve) => setTimeout(resolve, 500));
let filtered = [...mockCorrespondences];
if (params?.status) {
filtered = filtered.filter((c) => c.status === params.status);
}
if (params?.search) {
const lowerSearch = params.search.toLowerCase();
filtered = filtered.filter((c) =>
c.subject.toLowerCase().includes(lowerSearch) ||
c.document_number.toLowerCase().includes(lowerSearch)
);
}
return {
items: filtered,
total: filtered.length,
page: params?.page || 1,
totalPages: 1,
};
},
getById: async (id: number) => {
await new Promise((resolve) => setTimeout(resolve, 500));
return mockCorrespondences.find((c) => c.correspondence_id === id);
},
create: async (data: CreateCorrespondenceDto) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const newId = Math.max(...mockCorrespondences.map((c) => c.correspondence_id)) + 1;
const newCorrespondence: Correspondence = {
correspondence_id: newId,
document_number: `LCBP3-COR-00${newId}`,
...data,
status: "DRAFT",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
// Mock organizations for display
from_organization: { id: data.from_organization_id, org_name: "Mock Org From", org_code: "MOCK" },
to_organization: { id: data.to_organization_id, org_name: "Mock Org To", org_code: "MOCK" },
} as Correspondence; // Casting for simplicity in mock
mockCorrespondences.unshift(newCorrespondence);
return newCorrespondence;
},
};

View File

@@ -1,119 +0,0 @@
import { Drawing, CreateDrawingDto, DrawingRevision } from "@/types/drawing";
// Mock Data
const mockDrawings: Drawing[] = [
{
drawing_id: 1,
drawing_number: "A-101",
title: "Ground Floor Plan",
type: "CONTRACT",
discipline_id: 2,
discipline: { id: 2, discipline_code: "ARC", discipline_name: "Architecture" },
sheet_number: "01",
scale: "1:100",
current_revision: "0",
issue_date: new Date(Date.now() - 100000000).toISOString(),
revision_count: 1,
revisions: [
{
revision_id: 1,
revision_number: "0",
revision_date: new Date(Date.now() - 100000000).toISOString(),
revision_description: "Issued for Construction",
revised_by_name: "John Doe",
file_url: "/mock-drawing.pdf",
is_current: true,
},
],
},
{
drawing_id: 2,
drawing_number: "S-201",
title: "Foundation Details",
type: "SHOP",
discipline_id: 1,
discipline: { id: 1, discipline_code: "STR", discipline_name: "Structure" },
sheet_number: "05",
scale: "1:50",
current_revision: "B",
issue_date: new Date().toISOString(),
revision_count: 2,
revisions: [
{
revision_id: 3,
revision_number: "B",
revision_date: new Date().toISOString(),
revision_description: "Updated reinforcement",
revised_by_name: "Jane Smith",
file_url: "/mock-drawing-v2.pdf",
is_current: true,
},
{
revision_id: 2,
revision_number: "A",
revision_date: new Date(Date.now() - 50000000).toISOString(),
revision_description: "First Submission",
revised_by_name: "Jane Smith",
file_url: "/mock-drawing-v1.pdf",
is_current: false,
},
],
},
];
export const drawingApi = {
getAll: async (params?: { type?: "CONTRACT" | "SHOP"; search?: string }) => {
await new Promise((resolve) => setTimeout(resolve, 500));
let filtered = [...mockDrawings];
if (params?.type) {
filtered = filtered.filter((d) => d.type === params.type);
}
if (params?.search) {
const lowerSearch = params.search.toLowerCase();
filtered = filtered.filter((d) =>
d.drawing_number.toLowerCase().includes(lowerSearch) ||
d.title.toLowerCase().includes(lowerSearch)
);
}
return filtered;
},
getById: async (id: number) => {
await new Promise((resolve) => setTimeout(resolve, 500));
return mockDrawings.find((d) => d.drawing_id === id);
},
create: async (data: CreateDrawingDto) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const newId = Math.max(...mockDrawings.map((d) => d.drawing_id)) + 1;
const newDrawing: Drawing = {
drawing_id: newId,
drawing_number: data.drawing_number,
title: data.title,
type: data.drawing_type,
discipline_id: data.discipline_id,
discipline: { id: data.discipline_id, discipline_code: "MOCK", discipline_name: "Mock Discipline" },
sheet_number: data.sheet_number,
scale: data.scale,
current_revision: "0",
issue_date: new Date().toISOString(),
revision_count: 1,
revisions: [
{
revision_id: newId * 10,
revision_number: "0",
revision_date: new Date().toISOString(),
revision_description: "Initial Upload",
revised_by_name: "Current User",
file_url: "#",
is_current: true,
}
]
};
mockDrawings.unshift(newDrawing);
return newDrawing;
},
};

View File

@@ -1,98 +0,0 @@
import { RFA, CreateRFADto, RFAItem } from "@/types/rfa";
// Mock Data
const mockRFAs: RFA[] = [
{
rfa_id: 1,
rfa_number: "LCBP3-RFA-001",
subject: "Approval for Concrete Mix Design",
description: "Requesting approval for the proposed concrete mix design for foundations.",
contract_id: 1,
discipline_id: 1,
status: "PENDING",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
contract_name: "Main Construction Contract",
discipline_name: "Civil",
items: [
{ id: 1, item_no: "1.1", description: "Concrete Mix Type A", quantity: 1, unit: "Lot", status: "PENDING" },
{ id: 2, item_no: "1.2", description: "Concrete Mix Type B", quantity: 1, unit: "Lot", status: "PENDING" },
],
},
{
rfa_id: 2,
rfa_number: "LCBP3-RFA-002",
subject: "Approval for Steel Reinforcement Shop Drawings",
description: "Shop drawings for Zone A foundations.",
contract_id: 1,
discipline_id: 2,
status: "APPROVED",
created_at: new Date(Date.now() - 172800000).toISOString(),
updated_at: new Date(Date.now() - 86400000).toISOString(),
contract_name: "Main Construction Contract",
discipline_name: "Structural",
items: [
{ id: 3, item_no: "1", description: "Shop Drawing Set A", quantity: 1, unit: "Set", status: "APPROVED" },
],
},
];
export const rfaApi = {
getAll: async (params?: { page?: number; status?: string; search?: string }) => {
await new Promise((resolve) => setTimeout(resolve, 500));
let filtered = [...mockRFAs];
if (params?.status) {
filtered = filtered.filter((r) => r.status === params.status);
}
if (params?.search) {
const lowerSearch = params.search.toLowerCase();
filtered = filtered.filter((r) =>
r.subject.toLowerCase().includes(lowerSearch) ||
r.rfa_number.toLowerCase().includes(lowerSearch)
);
}
return {
items: filtered,
total: filtered.length,
page: params?.page || 1,
totalPages: 1,
};
},
getById: async (id: number) => {
await new Promise((resolve) => setTimeout(resolve, 500));
return mockRFAs.find((r) => r.rfa_id === id);
},
create: async (data: CreateRFADto) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const newId = Math.max(...mockRFAs.map((r) => r.rfa_id)) + 1;
const newRFA: RFA = {
rfa_id: newId,
rfa_number: `LCBP3-RFA-00${newId}`,
...data,
status: "DRAFT",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
contract_name: "Mock Contract",
discipline_name: "Mock Discipline",
items: data.items.map((item, index) => ({ ...item, id: index + 1, status: "PENDING" })),
};
mockRFAs.unshift(newRFA);
return newRFA;
},
updateStatus: async (id: number, status: RFA['status'], comments?: string) => {
await new Promise((resolve) => setTimeout(resolve, 800));
const rfa = mockRFAs.find((r) => r.rfa_id === id);
if (rfa) {
rfa.status = status;
rfa.updated_at = new Date().toISOString();
// In a real app, we'd log the comments and history
}
return rfa;
},
};

View File

@@ -1,79 +0,0 @@
import { SearchResult, SearchFilters } from "@/types/search";
// Mock Data
const mockResults: SearchResult[] = [
{
id: 1,
type: "correspondence",
title: "Submission of Monthly Report - Jan 2025",
description: "Please find attached the monthly progress report.",
status: "PENDING",
documentNumber: "LCBP3-COR-001",
createdAt: new Date().toISOString(),
highlight: "Submission of <b>Monthly Report</b> - Jan 2025",
},
{
id: 1,
type: "rfa",
title: "Approval for Concrete Mix Design",
description: "Requesting approval for the proposed concrete mix design.",
status: "PENDING",
documentNumber: "LCBP3-RFA-001",
createdAt: new Date().toISOString(),
highlight: "Approval for <b>Concrete Mix</b> Design",
},
{
id: 1,
type: "drawing",
title: "Ground Floor Plan",
description: "Architectural ground floor plan.",
status: "APPROVED",
documentNumber: "A-101",
createdAt: new Date(Date.now() - 100000000).toISOString(),
highlight: "Ground Floor <b>Plan</b>",
},
{
id: 2,
type: "correspondence",
title: "Request for Information regarding Foundation",
description: "Clarification needed on drawing A-101.",
status: "IN_REVIEW",
documentNumber: "LCBP3-COR-002",
createdAt: new Date(Date.now() - 86400000).toISOString(),
},
];
export const searchApi = {
search: async (filters: SearchFilters) => {
await new Promise((resolve) => setTimeout(resolve, 600));
let results = [...mockResults];
if (filters.query) {
const lowerQuery = filters.query.toLowerCase();
results = results.filter((r) =>
r.title.toLowerCase().includes(lowerQuery) ||
r.documentNumber.toLowerCase().includes(lowerQuery) ||
r.description?.toLowerCase().includes(lowerQuery)
);
}
if (filters.types && filters.types.length > 0) {
results = results.filter((r) => filters.types?.includes(r.type));
}
if (filters.statuses && filters.statuses.length > 0) {
results = results.filter((r) => filters.statuses?.includes(r.status));
}
return results;
},
suggest: async (query: string) => {
await new Promise((resolve) => setTimeout(resolve, 300));
const lowerQuery = query.toLowerCase();
return mockResults
.filter((r) => r.title.toLowerCase().includes(lowerQuery))
.slice(0, 5);
},
};

View File

@@ -122,6 +122,7 @@ export const {
return {
...token,
id: user.id,
username: user.username, // ✅ Save username
role: user.role,
organizationId: user.organizationId,
accessToken: user.accessToken,
@@ -141,6 +142,7 @@ export const {
async session({ session, token }) {
if (token && session.user) {
session.user.id = token.id as string;
session.user.username = token.username as string; // ✅ Restore username
session.user.role = token.role as string;
session.user.organizationId = token.organizationId as number;

View File

@@ -0,0 +1,20 @@
import apiClient from "@/lib/api/client";
export interface AuditLogRaw {
audit_log_id: number;
user_id: number;
user_name?: string;
action: string;
entity_type: string;
entity_id: string; // or number
description: string;
ip_address?: string;
created_at: string;
}
export const auditLogService = {
getLogs: async (params?: any) => {
const response = await apiClient.get<AuditLogRaw[]>("/audit-logs", { params });
return response.data;
}
};

View File

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

View File

@@ -0,0 +1,42 @@
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");
return response.data;
},
getRecentActivity: async (): Promise<ActivityLog[]> => {
try {
const response = await apiClient.get("/dashboard/activity");
// ตรวจสอบว่า response.data เป็น array จริงๆ
if (Array.isArray(response.data)) {
return response.data;
}
console.warn('Dashboard activity: expected array, got:', typeof response.data);
return [];
} catch (error) {
console.error('Failed to fetch recent activity:', error);
return [];
}
},
getPendingTasks: async (): Promise<PendingTask[]> => {
try {
const response = await apiClient.get("/dashboard/pending");
// Backend คืน { data: [], meta: {} } ต้องดึง data ออกมา
if (response.data?.data && Array.isArray(response.data.data)) {
return response.data.data;
}
if (Array.isArray(response.data)) {
return response.data;
}
console.warn('Dashboard pending: unexpected format:', typeof response.data);
return [];
} catch (error) {
console.error('Failed to fetch pending tasks:', error);
return [];
}
},
};

View File

@@ -6,10 +6,11 @@ import { CreateTagDto, UpdateTagDto, SearchTagDto } from "@/types/dto/master/tag
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 { Organization } from "@/types/organization";
export const masterDataService = {
// --- Tags Management ---
/** ดึงรายการ Tags ทั้งหมด (Search & Pagination) */
getTags: async (params?: SearchTagDto) => {
const response = await apiClient.get("/tags", { params });
@@ -34,19 +35,46 @@ export const masterDataService = {
return response.data;
},
// --- Organizations (Global) ---
/** ดึงรายชื่อองค์กรทั้งหมด */
getOrganizations: async () => {
const response = await apiClient.get<Organization[]>("/organizations");
return response.data;
},
/** สร้างองค์กรใหม่ */
createOrganization: async (data: any) => {
const response = await apiClient.post("/organizations", data);
return response.data;
},
/** แก้ไของค์กร */
updateOrganization: async (id: number, data: any) => {
const response = await apiClient.put(`/organizations/${id}`, data);
return response.data;
},
/** ลบองค์กร */
deleteOrganization: async (id: number) => {
const response = await apiClient.delete(`/organizations/${id}`);
return response.data;
},
// --- Disciplines Management (Admin / Req 6B) ---
/** ดึงรายชื่อสาขางาน (มักจะกรองตาม Contract ID) */
getDisciplines: async (contractId?: number) => {
const response = await apiClient.get("/disciplines", {
params: { contractId }
const response = await apiClient.get("/master/disciplines", {
params: { contractId }
});
return response.data;
},
/** สร้างสาขางานใหม่ */
createDiscipline: async (data: CreateDisciplineDto) => {
const response = await apiClient.post("/disciplines", data);
const response = await apiClient.post("/master/disciplines", data);
return response.data;
},
@@ -54,7 +82,7 @@ export const masterDataService = {
/** ดึงรายชื่อประเภทย่อย (กรองตาม Contract และ Type) */
getSubTypes: async (contractId?: number, typeId?: number) => {
const response = await apiClient.get("/sub-types", {
const response = await apiClient.get("/master/sub-types", {
params: { contractId, correspondenceTypeId: typeId }
});
return response.data;
@@ -62,7 +90,7 @@ export const masterDataService = {
/** สร้างประเภทย่อยใหม่ */
createSubType: async (data: CreateSubTypeDto) => {
const response = await apiClient.post("/sub-types", data);
const response = await apiClient.post("/master/sub-types", data);
return response.data;
},
@@ -81,4 +109,4 @@ export const masterDataService = {
});
return response.data;
}
};
};

View File

@@ -1,48 +1,21 @@
// File: lib/services/notification.service.ts
import apiClient from "@/lib/api/client";
import {
SearchNotificationDto,
CreateNotificationDto
} from "@/types/dto/notification/notification.dto";
import { NotificationResponse } from "@/types/notification";
export const notificationService = {
/** * ดึงรายการแจ้งเตือนของผู้ใช้ปัจจุบัน
* GET /notifications
*/
getMyNotifications: async (params?: SearchNotificationDto) => {
const response = await apiClient.get("/notifications", { params });
getUnread: async (): Promise<NotificationResponse> => {
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;
},
/** * สร้างการแจ้งเตือนใหม่ (มักใช้โดย 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) => {
markAsRead: async (id: number) => {
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}`);
const response = await apiClient.patch(`/notifications/read-all`);
return response.data;
}
};
};

View File

@@ -10,12 +10,24 @@ 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;
},
/**
* Suggestion (Autocomplete)
* ใช้ search endpoint แต่จำกัดจำนวน
*/
suggest: async (query: string) => {
const response = await apiClient.get("/search", {
params: { q: query, limit: 5 }
});
// Assuming backend returns { items: [], ... } or just []
return response.data.items || response.data;
},
/**
* (Optional) Re-index ข้อมูลใหม่ กรณีข้อมูลไม่ตรง (Admin Only)
*/
@@ -23,4 +35,4 @@ export const searchService = {
const response = await apiClient.post("/search/reindex", { type });
return response.data;
}
};
};

View File

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

View File

@@ -1,57 +1,35 @@
// File: lib/services/user.service.ts
import apiClient from "@/lib/api/client";
import {
CreateUserDto,
UpdateUserDto,
AssignRoleDto,
UpdatePreferenceDto
} from "@/types/dto/user/user.dto";
import { CreateUserDto, UpdateUserDto, SearchUserDto, User } from "@/types/user";
export const userService = {
/** ดึงรายชื่อผู้ใช้ทั้งหมด (Admin) */
getAll: async (params?: any) => {
const response = await apiClient.get("/users", { params });
getAll: async (params?: SearchUserDto) => {
const response = await apiClient.get<User[]>("/users", { params });
// Assuming backend returns array or paginated object.
// If backend uses standard pagination { data: [], total: number }, adjust accordingly.
// Based on previous code checks, it seems simple array or standard structure.
// Let's assume standard response for now.
return response.data;
},
/** ดึงข้อมูลผู้ใช้ตาม ID */
getById: async (id: number | string) => {
const response = await apiClient.get(`/users/${id}`);
getById: async (id: number) => {
const response = await apiClient.get<User>(`/users/${id}`);
return response.data;
},
/** สร้างผู้ใช้ใหม่ (Admin) */
create: async (data: CreateUserDto) => {
const response = await apiClient.post("/users", data);
const response = await apiClient.post<User>("/users", data);
return response.data;
},
/** แก้ไขข้อมูลผู้ใช้ */
update: async (id: number | string, data: UpdateUserDto) => {
const response = await apiClient.put(`/users/${id}`, data);
update: async (id: number, data: UpdateUserDto) => {
const response = await apiClient.put<User>(`/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) => {
delete: async (id: number) => {
const response = await apiClient.delete(`/users/${id}`);
return response.data;
}
};
},
// Optional: Reset Password, Deactivate etc.
};

View File

@@ -0,0 +1,61 @@
// File: lib/stores/auth-store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface User {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
role: string | 'User' | 'Admin' | 'Viewer';
permissions?: string[];
}
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
setAuth: (user: User, token: string) => void;
logout: () => void;
hasPermission: (permission: string) => boolean;
hasRole: (role: string) => boolean;
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
user: null,
token: null,
isAuthenticated: false,
setAuth: (user, token) => {
set({ user, token, isAuthenticated: true });
},
logout: () => {
set({ user: null, token: null, isAuthenticated: false });
},
hasPermission: (requiredPermission: string) => {
const { user } = get();
if (!user) return false;
if (user.permissions?.includes(requiredPermission)) return true;
if (user.role === 'Admin') return true;
return false;
},
hasRole: (requiredRole: string) => {
const { user } = get();
return user?.role === requiredRole;
}
}),
{
name: 'auth-storage',
}
)
);