251208:0010 Backend & Frontend Debug
This commit is contained in:
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
|
||||
20
frontend/lib/services/audit-log.service.ts
Normal file
20
frontend/lib/services/audit-log.service.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
42
frontend/lib/services/dashboard.service.ts
Normal file
42
frontend/lib/services/dashboard.service.ts
Normal 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 [];
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
};
|
||||
|
||||
61
frontend/lib/stores/auth-store.ts
Normal file
61
frontend/lib/stores/auth-store.ts
Normal 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',
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user