260224:1606 20260224:1600 V1.8.0
All checks were successful
Build and Deploy / deploy (push) Successful in 6m25s

This commit is contained in:
admin
2026-02-24 16:06:15 +07:00
parent 97cc41f489
commit 158179d4a5
255 changed files with 5339 additions and 2094 deletions

View File

@@ -42,24 +42,24 @@ interface FieldConfig {
label: string;
type: "text" | "textarea" | "checkbox" | "select";
required?: boolean;
options?: { label: string; value: any }[];
options?: { label: string; value: string | number | boolean }[];
}
interface GenericCrudTableProps {
interface GenericCrudTableProps<TEntity extends { id: number }> {
entityName: string;
queryKey: string[];
fetchFn: () => Promise<any>;
createFn: (data: any) => Promise<any>;
updateFn: (id: number, data: any) => Promise<any>;
deleteFn: (id: number) => Promise<any>;
columns: ColumnDef<any>[];
fetchFn: () => Promise<TEntity[]>;
createFn: (data: Record<string, unknown>) => Promise<TEntity>;
updateFn: (id: number, data: Record<string, unknown>) => Promise<TEntity>;
deleteFn: (id: number) => Promise<unknown>;
columns: ColumnDef<TEntity>[];
fields: FieldConfig[];
title?: string;
description?: string;
filters?: React.ReactNode;
}
export function GenericCrudTable({
export function GenericCrudTable<TEntity extends { id: number }>({
entityName,
queryKey,
fetchFn,
@@ -71,11 +71,11 @@ export function GenericCrudTable({
title,
description,
filters,
}: GenericCrudTableProps) {
}: GenericCrudTableProps<TEntity>) {
const queryClient = useQueryClient();
const [isOpen, setIsOpen] = useState(false);
const [editingItem, setEditingItem] = useState<any>(null);
const [formData, setFormData] = useState<any>({});
const [editingItem, setEditingItem] = useState<TEntity | null>(null);
const [formData, setFormData] = useState<Record<string, unknown>>({});
// Delete Dialog State
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -97,7 +97,7 @@ export function GenericCrudTable({
});
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => updateFn(id, data),
mutationFn: ({ id, data }: { id: number; data: Record<string, unknown> }) => updateFn(id, data),
onSuccess: () => {
toast.success(`${entityName} updated successfully`);
queryClient.invalidateQueries({ queryKey });
@@ -123,7 +123,7 @@ export function GenericCrudTable({
setIsOpen(true);
};
const handleEdit = (item: any) => {
const handleEdit = (item: TEntity) => {
setEditingItem(item);
setFormData({ ...item });
setIsOpen(true);
@@ -155,8 +155,8 @@ export function GenericCrudTable({
}
};
const handleChange = (field: string, value: any) => {
setFormData((prev: any) => ({ ...prev, [field]: value }));
const handleChange = (field: string, value: unknown) => {
setFormData((prev: Record<string, unknown>) => ({ ...prev, [field]: value }));
};
// Add default Actions column if not present
@@ -165,7 +165,7 @@ export function GenericCrudTable({
{
id: "actions",
header: "Actions",
cell: ({ row }: { row: any }) => (
cell: ({ row }: { row: { original: TEntity } }) => (
<div className="flex gap-2 justify-end">
<Button
variant="ghost"
@@ -239,7 +239,7 @@ export function GenericCrudTable({
{field.type === "textarea" ? (
<Textarea
id={field.name}
value={formData[field.name] || ""}
value={(formData[field.name] as string) || ""}
onChange={(e) => handleChange(field.name, e.target.value)}
required={field.required}
/>
@@ -266,7 +266,7 @@ export function GenericCrudTable({
</SelectTrigger>
<SelectContent>
{field.options?.map((opt) => (
<SelectItem key={opt.value} value={opt.value.toString()}>
<SelectItem key={(opt.value as string | number)} value={opt.value.toString()}>
{opt.label}
</SelectItem>
))}
@@ -276,7 +276,7 @@ export function GenericCrudTable({
<Input
id={field.name}
type="text"
value={formData[field.name] || ""}
value={(formData[field.name] as string) || ""}
onChange={(e) => handleChange(field.name, e.target.value)}
required={field.required}
/>

View File

@@ -14,6 +14,14 @@ import {
} from "@/components/ui/popover";
import { useSearchSuggestions } from "@/hooks/use-search";
/** Search suggestion item returned from the API */
interface SearchSuggestion {
id: string | number;
type: string;
title: string;
documentNumber?: string;
}
function useDebounceValue<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
@@ -87,7 +95,7 @@ export function GlobalSearch() {
<CommandList>
{suggestions && suggestions.length > 0 && (
<CommandGroup heading="Suggestions">
{suggestions.map((item: any) => (
{(suggestions as SearchSuggestion[]).map((item) => (
<CommandItem
key={`${item.type}-${item.id}`}
onSelect={() => {

View File

@@ -14,6 +14,7 @@ import { Badge } from "@/components/ui/badge";
import { useNotifications, useMarkNotificationRead } from "@/hooks/use-notification";
import { formatDistanceToNow } from "date-fns";
import { useRouter } from "next/navigation";
import type { Notification } from "@/types/notification";
export function NotificationsDropdown() {
const router = useRouter();
@@ -23,7 +24,7 @@ export function NotificationsDropdown() {
const notifications = data?.items || [];
const unreadCount = data?.unreadCount || 0;
const handleNotificationClick = (notification: any) => {
const handleNotificationClick = (notification: Notification) => {
if (!notification.isRead) {
markAsRead.mutate(notification.notificationId);
}
@@ -62,7 +63,7 @@ export function NotificationsDropdown() {
</div>
) : (
<div className="max-h-96 overflow-y-auto">
{notifications.slice(0, 5).map((notification: any) => (
{notifications.slice(0, 5).map((notification: Notification) => (
<DropdownMenuItem
key={notification.notificationId}
className={`flex flex-col items-start p-3 cursor-pointer ${

View File

@@ -1,12 +1,13 @@
import { AuditLog, AuditLogQueryParams } from '@/lib/services/audit-log.service';
import { useQuery } from '@tanstack/react-query';
import { auditLogService, AuditLog } from '@/lib/services/audit-log.service';
import { auditLogService } from '@/lib/services/audit-log.service';
export const auditLogKeys = {
all: ['audit-logs'] as const,
list: (params: any) => [...auditLogKeys.all, 'list', params] as const,
list: (params?: AuditLogQueryParams) => [...auditLogKeys.all, 'list', params] as const,
};
export function useAuditLogs(params?: any) {
export function useAuditLogs(params?: AuditLogQueryParams) {
return useQuery<AuditLog[]>({
queryKey: auditLogKeys.list(params),
queryFn: () => auditLogService.getLogs(params),

View File

@@ -37,7 +37,7 @@ export const useNumberingMetrics = () => {
export const useNumberingAuditLogs = (params?: AuditQueryParams) => {
return useQuery({
queryKey: numberingKeys.auditLogs(params),
queryFn: () => documentNumberingService.getAuditLogs(params),
queryFn: () => documentNumberingService.getAuditLogs(),
});
};
@@ -75,7 +75,7 @@ export const useCancelNumbering = () => {
export const useBulkImportNumbering = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: FormData | any[]) => documentNumberingService.bulkImport(data),
mutationFn: (data: FormData | { documentNumber: string; projectId: number; sequenceNumber: number }[]) => documentNumberingService.bulkImport(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: numberingKeys.all });
},

View File

@@ -2,6 +2,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { projectService } from '@/lib/services/project.service';
import { CreateProjectDto, UpdateProjectDto, SearchProjectDto } from '@/types/dto/project/project.dto';
import { toast } from 'sonner';
import { getApiErrorMessage } from '@/types/api-error';
export const projectKeys = {
all: ['projects'] as const,
@@ -24,9 +25,9 @@ export function useCreateProject() {
toast.success("Project created successfully");
queryClient.invalidateQueries({ queryKey: projectKeys.all });
},
onError: (error: any) => {
onError: (error: unknown) => {
toast.error("Failed to create project", {
description: error.response?.data?.message || "Unknown error"
description: getApiErrorMessage(error, "Unknown error")
});
}
});
@@ -40,9 +41,9 @@ export function useUpdateProject() {
toast.success("Project updated successfully");
queryClient.invalidateQueries({ queryKey: projectKeys.all });
},
onError: (error: any) => {
onError: (error: unknown) => {
toast.error("Failed to update project", {
description: error.response?.data?.message || "Unknown error"
description: getApiErrorMessage(error, "Unknown error")
});
}
});
@@ -56,9 +57,9 @@ export function useDeleteProject() {
toast.success("Project deleted successfully");
queryClient.invalidateQueries({ queryKey: projectKeys.all });
},
onError: (error: any) => {
onError: (error: unknown) => {
toast.error("Failed to delete project", {
description: error.response?.data?.message || "Unknown error"
description: getApiErrorMessage(error, "Unknown error")
});
}
});

View File

@@ -1,5 +1,8 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { masterDataService } from '@/lib/services/master-data.service';
import type { CreateDisciplineDto } from '@/types/dto/master/discipline.dto';
import type { CreateRfaTypeDto, UpdateRfaTypeDto } from '@/types/dto/master/rfa-type.dto';
import type { CreateCorrespondenceTypeDto, UpdateCorrespondenceTypeDto } from '@/types/dto/master/correspondence-type.dto';
export const referenceDataKeys = {
all: ['reference-data'] as const,
@@ -19,7 +22,7 @@ export const useRfaTypes = (contractId?: number) => {
export const useCreateRfaType = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => masterDataService.createRfaType(data),
mutationFn: (data: CreateRfaTypeDto) => masterDataService.createRfaType(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'rfaTypes'] });
},
@@ -29,7 +32,7 @@ export const useCreateRfaType = () => {
export const useUpdateRfaType = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => masterDataService.updateRfaType(id, data),
mutationFn: ({ id, data }: { id: number; data: UpdateRfaTypeDto }) => masterDataService.updateRfaType(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'rfaTypes'] });
},
@@ -57,7 +60,7 @@ export const useDisciplines = (contractId?: number) => {
export const useCreateDiscipline = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => masterDataService.createDiscipline(data),
mutationFn: (data: CreateDisciplineDto) => masterDataService.createDiscipline(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'disciplines'] });
},
@@ -85,7 +88,7 @@ export const useCorrespondenceTypes = () => {
export const useCreateCorrespondenceType = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => masterDataService.createCorrespondenceType(data),
mutationFn: (data: CreateCorrespondenceTypeDto) => masterDataService.createCorrespondenceType(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'correspondenceTypes'] });
},
@@ -95,7 +98,7 @@ export const useCreateCorrespondenceType = () => {
export const useUpdateCorrespondenceType = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: any }) => masterDataService.updateCorrespondenceType(id, data),
mutationFn: ({ id, data }: { id: number; data: UpdateCorrespondenceTypeDto }) => masterDataService.updateCorrespondenceType(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['reference-data', 'correspondenceTypes'] });
},

View File

@@ -3,6 +3,7 @@ import { rfaService } from '@/lib/services/rfa.service';
import { SearchRfaDto, CreateRfaDto, UpdateRfaDto } from '@/types/dto/rfa/rfa.dto';
import { WorkflowActionDto } from '@/lib/services/rfa.service';
import { toast } from 'sonner';
import { getApiErrorMessage } from '@/types/api-error';
// Keys
export const rfaKeys = {
@@ -42,9 +43,9 @@ export function useCreateRFA() {
toast.success('RFA created successfully');
queryClient.invalidateQueries({ queryKey: rfaKeys.lists() });
},
onError: (error: any) => {
onError: (error: unknown) => {
toast.error('Failed to create RFA', {
description: error.response?.data?.message || 'Something went wrong',
description: getApiErrorMessage(error, 'Something went wrong'),
});
},
});
@@ -61,9 +62,9 @@ export function useUpdateRFA() {
queryClient.invalidateQueries({ queryKey: rfaKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: rfaKeys.lists() });
},
onError: (error: any) => {
onError: (error: unknown) => {
toast.error('Failed to update RFA', {
description: error.response?.data?.message || 'Something went wrong',
description: getApiErrorMessage(error, 'Something went wrong'),
});
},
});
@@ -80,9 +81,9 @@ export function useProcessRFA() {
queryClient.invalidateQueries({ queryKey: rfaKeys.detail(id) });
queryClient.invalidateQueries({ queryKey: rfaKeys.lists() });
},
onError: (error: any) => {
onError: (error: unknown) => {
toast.error('Failed to process workflow', {
description: error.response?.data?.message || 'Something went wrong',
description: getApiErrorMessage(error, 'Something went wrong'),
});
},
});

View File

@@ -1,11 +1,12 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userService } from '@/lib/services/user.service';
import { CreateUserDto, UpdateUserDto, SearchUserDto } from '@/types/user'; // Ensure types exist
import { CreateUserDto, UpdateUserDto, SearchUserDto } from '@/types/user';
import { toast } from 'sonner';
import { getApiErrorMessage } from '@/types/api-error';
export const userKeys = {
all: ['users'] as const,
list: (params: any) => [...userKeys.all, 'list', params] as const,
list: (params?: SearchUserDto) => [...userKeys.all, 'list', params] as const,
detail: (id: number) => [...userKeys.all, 'detail', id] as const,
};
@@ -31,9 +32,9 @@ export function useCreateUser() {
toast.success("User created successfully");
queryClient.invalidateQueries({ queryKey: userKeys.all });
},
onError: (error: any) => {
onError: (error: unknown) => {
toast.error("Failed to create user", {
description: error.response?.data?.message || "Unknown error"
description: getApiErrorMessage(error, "Unknown error")
});
}
});
@@ -47,9 +48,9 @@ export function useUpdateUser() {
toast.success("User updated successfully");
queryClient.invalidateQueries({ queryKey: userKeys.all });
},
onError: (error: any) => {
onError: (error: unknown) => {
toast.error("Failed to update user", {
description: error.response?.data?.message || "Unknown error"
description: getApiErrorMessage(error, "Unknown error")
});
}
});
@@ -63,9 +64,9 @@ export function useDeleteUser() {
toast.success("User deleted successfully");
queryClient.invalidateQueries({ queryKey: userKeys.all });
},
onError: (error: any) => {
onError: (error: unknown) => {
toast.error("Failed to delete user", {
description: error.response?.data?.message || "Unknown error"
description: getApiErrorMessage(error, "Unknown error")
});
}
});

View File

@@ -3,6 +3,7 @@ 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({
@@ -17,12 +18,12 @@ function getJwtExpiry(token: string): number {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.exp * 1000; // Convert to ms
} catch (e) {
} catch {
return Date.now(); // If invalid, treat as expired
}
}
async function refreshAccessToken(token: any) {
async function refreshAccessToken(token: JWT) {
try {
const response = await fetch(`${baseUrl}/auth/refresh`, {
method: "POST",

View File

@@ -1,4 +1,5 @@
import apiClient from "@/lib/api/client";
import { AuditQueryParams } from '@/types/dto/numbering.dto';
export interface AuditLog {
auditId: string;
@@ -12,16 +13,18 @@ export interface AuditLog {
severity: string;
entityType?: string;
entityId?: string;
detailsJson?: any;
detailsJson?: Record<string, unknown>;
ipAddress?: string;
userAgent?: string;
createdAt: string;
}
export type AuditLogQueryParams = AuditQueryParams;
export const auditLogService = {
getLogs: async (params?: any) => {
const response = await apiClient.get<any>("/audit-logs", { params });
getLogs: async (params?: AuditLogQueryParams) => {
const response = await apiClient.get<{ data: AuditLog[] } | AuditLog[]>("/audit-logs", { params });
// Support both wrapped and unwrapped scenarios
return response.data.data || response.data;
return (response.data as { data: AuditLog[] }).data ?? response.data;
},
};

View File

@@ -25,7 +25,7 @@ export const correspondenceService = {
return response.data;
},
update: async (id: string | number, data: any) => {
update: async (id: string | number, data: Partial<CreateCorrespondenceDto>) => {
const response = await apiClient.put(`/correspondences/${id}`, data);
return response.data;
},

View File

@@ -4,9 +4,16 @@ import {
ManualOverrideDto,
VoidReplaceDto,
CancelNumberDto,
AuditQueryParams
} from "@/types/dto/numbering.dto";
/** A bulk-import record row */
export interface BulkImportRecord {
documentNumber: string;
projectId: number;
sequenceNumber: number;
[key: string]: unknown;
}
export const documentNumberingService = {
// --- Admin Dashboard Metrics ---
getMetrics: async (): Promise<NumberingMetrics> => {
@@ -19,7 +26,7 @@ export const documentNumberingService = {
await apiClient.post("/admin/document-numbering/manual-override", dto);
},
voidAndReplace: async (dto: VoidReplaceDto): Promise<any> => {
voidAndReplace: async (dto: VoidReplaceDto): Promise<{ documentNumber: string }> => {
const response = await apiClient.post("/admin/document-numbering/void-and-replace", dto);
return response.data;
},
@@ -28,7 +35,7 @@ export const documentNumberingService = {
await apiClient.post("/admin/document-numbering/cancel", dto);
},
bulkImport: async (data: FormData | any[]): Promise<any> => {
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);
@@ -36,7 +43,7 @@ export const documentNumberingService = {
},
// --- Audit Logs ---
getAuditLogs: async (params?: AuditQueryParams) => {
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:

View File

@@ -1,9 +1,9 @@
// File: lib/services/json-schema.service.ts
import apiClient from "@/lib/api/client";
import {
CreateJsonSchemaDto,
UpdateJsonSchemaDto,
SearchJsonSchemaDto
import {
CreateJsonSchemaDto,
UpdateJsonSchemaDto,
SearchJsonSchemaDto
} from "@/types/dto/json-schema/json-schema.dto";
export const jsonSchemaService = {
@@ -64,7 +64,7 @@ export const jsonSchemaService = {
/**
* (Optional) ตรวจสอบความถูกต้องของข้อมูลกับ Schema ฝั่ง Server
*/
validate: async (code: string, data: any) => {
validate: async (code: string, data: Record<string, unknown>) => {
// POST /json-schemas/validate
const response = await apiClient.post(`/json-schemas/validate`, {
schemaCode: code,
@@ -72,4 +72,4 @@ export const jsonSchemaService = {
});
return response.data; // { valid: true, errors: [] }
}
};
};

View File

@@ -6,6 +6,8 @@ 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 { 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,
@@ -137,21 +139,11 @@ export const masterDataService = {
},
/** สร้างประเภท RFA ใหม่ */
createRfaType: async (data: any) => {
// Note: Assuming endpoint is /master/rfa-types (POST)
// Currently RfaController handles /rfas, but master data usually goes to MasterController or dedicated
// The previous implementation used direct apiClient calls in the page.
// Let's assume we use the endpoint we just updated in MasterController which is GET only?
// Wait, MasterController doesn't have createRfaType.
// Let's check where RFA Types are created. RfaController creates RFAs (documents).
// RFA Types are likely master data.
// I need to add create/update/delete endpoints for RFA Types to MasterController if they don't exist.
// Checking MasterController again... it DOES NOT have createRfaType.
// I will add them to MasterController first.
createRfaType: async (data: CreateRfaTypeDto) => {
return apiClient.post("/master/rfa-types", data).then(res => res.data);
},
updateRfaType: async (id: number, data: any) => {
updateRfaType: async (id: number, data: UpdateRfaTypeDto) => {
return apiClient.patch(`/master/rfa-types/${id}`, data).then(res => res.data);
},
@@ -167,11 +159,11 @@ export const masterDataService = {
return response.data.data || response.data;
},
createCorrespondenceType: async (data: any) => {
createCorrespondenceType: async (data: CreateCorrespondenceTypeDto) => {
return apiClient.post("/master/correspondence-types", data).then(res => res.data);
},
updateCorrespondenceType: async (id: number, data: any) => {
updateCorrespondenceType: async (id: number, data: UpdateCorrespondenceTypeDto) => {
return apiClient.patch(`/master/correspondence-types/${id}`, data).then(res => res.data);
},

View File

@@ -16,8 +16,8 @@ export interface Session {
export const sessionService = {
getActiveSessions: async () => {
const response = await apiClient.get<any>('/auth/sessions');
return response.data.data || response.data;
const response = await apiClient.get<Session[] | { data: Session[] }>('/auth/sessions');
return (response.data as { data: Session[] }).data ?? response.data;
},
revokeSession: async (sessionId: number) => {

View File

@@ -1,54 +1,62 @@
import apiClient from "@/lib/api/client";
import { CreateUserDto, UpdateUserDto, SearchUserDto, User } from "@/types/user";
const transformUser = (user: any): User => {
/** Raw API user shape (before transform) */
interface RawUser {
user_id?: number;
userId?: number;
assignments?: Array<{ role: unknown }>;
[key: string]: unknown;
}
const transformUser = (user: RawUser): User => {
return {
...user,
userId: user.user_id,
roles: user.assignments?.map((a: any) => a.role) || [],
...(user as unknown as User),
userId: (user.user_id ?? user.userId) as number,
roles: (user.assignments?.map((a) => a.role) ?? []) as User['roles'],
};
};
/** Paginated or unwrapped response shape */
type UserListResponse = User[] | { data: User[] | { data: User[] } };
export const userService = {
getAll: async (params?: SearchUserDto) => {
const response = await apiClient.get<any>("/users", { params });
const response = await apiClient.get<UserListResponse>("/users", { params });
// Handle both paginated and non-paginated responses
let rawData = response.data?.data || response.data;
// If paginated (has .data property which is array)
if (rawData && Array.isArray(rawData.data)) {
rawData = rawData.data;
let rawData: RawUser[] | unknown = response.data;
if (rawData && !Array.isArray(rawData) && 'data' in (rawData as object)) {
rawData = (rawData as { data: unknown }).data;
}
// If still not array (e.g. error or empty), default to []
if (!Array.isArray(rawData)) {
return [];
if (rawData && !Array.isArray(rawData) && typeof rawData === 'object' && 'data' in (rawData as object)) {
rawData = (rawData as { data: unknown }).data;
}
if (!Array.isArray(rawData)) return [];
return rawData.map(transformUser);
return (rawData as RawUser[]).map(transformUser);
},
getRoles: async () => {
const response = await apiClient.get<any>("/users/roles");
if (response.data?.data) {
return response.data.data;
const response = await apiClient.get<{ data: unknown } | unknown>("/users/roles");
if (response.data && typeof response.data === 'object' && 'data' in (response.data as object)) {
return (response.data as { data: unknown }).data;
}
return response.data;
},
getById: async (id: number) => {
const response = await apiClient.get<User>(`/users/${id}`);
const response = await apiClient.get<RawUser>(`/users/${id}`);
return transformUser(response.data);
},
create: async (data: CreateUserDto) => {
const response = await apiClient.post<User>("/users", data);
const response = await apiClient.post<RawUser>("/users", data);
return transformUser(response.data);
},
update: async (id: number, data: UpdateUserDto) => {
const response = await apiClient.put<User>(`/users/${id}`, data);
const response = await apiClient.put<RawUser>(`/users/${id}`, data);
return transformUser(response.data);
},

View File

@@ -2,10 +2,13 @@
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
/** A draft can hold any serializable form data — typed as unknown for strictness */
type DraftValue = Record<string, unknown>;
interface DraftState {
drafts: Record<string, any>;
saveDraft: (key: string, data: any) => void;
getDraft: (key: string) => any;
drafts: Record<string, DraftValue>;
saveDraft: (key: string, data: DraftValue) => void;
getDraft: (key: string) => DraftValue | undefined;
clearDraft: (key: string) => void;
}
@@ -26,4 +29,4 @@ export const useDraftStore = create<DraftState>()(
storage: createJSONStorage(() => localStorage),
}
)
);
);

View File

@@ -0,0 +1,20 @@
// Shared API error type for Axios-based error handling in TanStack Query
export interface ApiErrorResponse {
message?: string;
error?: string;
statusCode?: number;
}
/** Axios-compatible error with typed response data */
export interface ApiError extends Error {
response?: {
data?: ApiErrorResponse;
status?: number;
};
}
/** Extract human-readable message from API error */
export function getApiErrorMessage(error: unknown, fallback = 'An unexpected error occurred'): string {
const apiError = error as ApiError;
return apiError?.response?.data?.message ?? apiError?.message ?? fallback;
}

View File

@@ -30,7 +30,7 @@ export interface CorrespondenceRevision {
statusCode: string;
statusName: string;
};
details?: any;
details?: Record<string, unknown> | null;
attachments?: Attachment[];
createdAt: string;
@@ -80,7 +80,7 @@ export interface CreateCorrespondenceDto {
remarks?: string;
dueDate?: string;
description?: string;
details?: Record<string, any>;
details?: Record<string, unknown>;
isInternal?: boolean;
originatorId?: number;
recipients?: { organizationId: number; type: 'TO' | 'CC' }[];

View File

@@ -7,7 +7,7 @@ export interface CreateContractDto {
endDate?: string;
}
export interface UpdateContractDto extends Partial<CreateContractDto> {}
export type UpdateContractDto = Partial<CreateContractDto>;
export interface SearchContractDto {
search?: string;

View File

@@ -29,7 +29,7 @@ export interface CreateCorrespondenceDto {
dueDate?: string;
/** ข้อมูล JSON เฉพาะประเภท (เช่น RFI question, RFA details) */
details?: Record<string, any>;
details?: Record<string, unknown>;
/** เอกสารภายในหรือไม่ (True = ภายใน) */
isInternal?: boolean;

View File

@@ -25,7 +25,7 @@ export interface CreateContractDrawingDto {
}
// --- Update (Partial) ---
export interface UpdateContractDrawingDto extends Partial<CreateContractDrawingDto> {}
export type UpdateContractDrawingDto = Partial<CreateContractDrawingDto>;
// --- Search ---
export interface SearchContractDrawingDto {

View File

@@ -9,14 +9,14 @@ export interface CreateJsonSchemaDto {
version?: number;
/** โครงสร้าง JSON Schema (Standard Format) */
schemaDefinition: Record<string, any>;
schemaDefinition: Record<string, unknown>;
/** สถานะการใช้งาน */
isActive?: boolean;
}
// --- Update (Partial) ---
export interface UpdateJsonSchemaDto extends Partial<CreateJsonSchemaDto> {}
export type UpdateJsonSchemaDto = Partial<CreateJsonSchemaDto>;
// --- Search ---
export interface SearchJsonSchemaDto {
@@ -31,4 +31,4 @@ export interface SearchJsonSchemaDto {
/** จำนวนต่อหน้า (Default: 20) */
limit?: number;
}
}

View File

@@ -0,0 +1,24 @@
// File: src/types/dto/master/correspondence-type.dto.ts
export interface CreateCorrespondenceTypeDto {
/** ชื่อประเภทเอกสาร (เช่น 'Letter', 'RFA') */
typeName: string;
/** รหัสประเภทเอกสาร (เช่น 'LTR', 'RFA') */
typeCode: string;
/** มีการออกเลขเอกสารอัตโนมัติหรือไม่ */
hasNumbering?: boolean;
/** สถานะการใช้งาน (Default: true) */
isActive?: boolean;
}
export type UpdateCorrespondenceTypeDto = Partial<CreateCorrespondenceTypeDto>;
export interface SearchCorrespondenceTypeDto {
search?: string;
isActive?: boolean;
page?: number;
limit?: number;
}

View File

@@ -0,0 +1,21 @@
// File: src/types/dto/master/rfa-type.dto.ts
export interface CreateRfaTypeDto {
/** ชื่อประเภท RFA (เช่น 'Drawing Approval') */
typeName: string;
/** รหัสประเภท RFA (เช่น 'DWG', 'MAT') */
typeCode: string;
/** สถานะการใช้งาน (Default: true) */
isActive?: boolean;
}
export type UpdateRfaTypeDto = Partial<CreateRfaTypeDto>;
export interface SearchRfaTypeDto {
search?: string;
isActive?: boolean;
page?: number;
limit?: number;
}

View File

@@ -8,7 +8,7 @@ export interface CreateTagDto {
description?: string;
}
export interface UpdateTagDto extends Partial<CreateTagDto> {}
export type UpdateTagDto = Partial<CreateTagDto>;
export interface SearchTagDto {
/** คำค้นหา (ชื่อ Tag หรือ คำอธิบาย) */
@@ -19,4 +19,4 @@ export interface SearchTagDto {
/** จำนวนรายการต่อหน้า (Default: 20) */
limit?: number;
}
}

View File

@@ -1,6 +1,15 @@
import type { AuditLog } from '@/lib/services/audit-log.service';
export interface AuditErrorRecord {
code: string;
message: string;
timestamp: string;
context?: Record<string, unknown>;
}
export interface NumberingMetrics {
audit: any[]; // Replace with specific AuditLog type if available
errors: any[]; // Replace with specific ErrorLog type
audit: AuditLog[];
errors: AuditErrorRecord[];
}
export interface ManualOverrideDto {

View File

@@ -13,7 +13,7 @@ export interface CreateProjectDto {
}
// --- Update (Partial) ---
export interface UpdateProjectDto extends Partial<CreateProjectDto> {}
export type UpdateProjectDto = Partial<CreateProjectDto>;
// --- Search ---
export interface SearchProjectDto {
@@ -28,4 +28,4 @@ export interface SearchProjectDto {
/** จำนวนรายการต่อหน้า (Default: 20) */
limit?: number;
}
}

View File

@@ -37,7 +37,7 @@ export interface CreateRfaDto {
}
// --- Update (Partial) ---
export interface UpdateRfaDto extends Partial<CreateRfaDto> {}
export type UpdateRfaDto = Partial<CreateRfaDto>;
// --- Search ---
export interface SearchRfaDto {

View File

@@ -25,7 +25,7 @@ export interface CreateTransmittalItemDto {
}
// --- Update (Partial) ---
export interface UpdateTransmittalDto extends Partial<CreateTransmittalDto> {}
export type UpdateTransmittalDto = Partial<CreateTransmittalDto>;
// --- Search ---
export interface SearchTransmittalDto {

View File

@@ -13,13 +13,13 @@ export interface CreateUserDto {
}
// --- Update User ---
export interface UpdateUserDto extends Partial<CreateUserDto> {}
export type UpdateUserDto = Partial<CreateUserDto>;
// --- Assign Role ---
export interface AssignRoleDto {
userId: number;
roleId: number;
// Scope (Optional)
organizationId?: number;
projectId?: number;
@@ -32,4 +32,4 @@ export interface UpdatePreferenceDto {
notifyLine?: boolean;
digestMode?: boolean;
uiTheme?: 'light' | 'dark' | 'system';
}
}

View File

@@ -1,19 +1,42 @@
// File: src/types/dto/workflow-engine/workflow-engine.dto.ts
/** DSL JSON structure representing a Workflow definition.
* Uses an open-ended signature to support diverse workflow types (YAML-derived, visual, etc.)
*/
export interface WorkflowDsl {
/** Allow extra properties for different DSL formats */
[key: string]: unknown;
states?: Record<string, WorkflowState>;
initial_state?: string;
}
export interface WorkflowState {
transitions?: WorkflowTransition[];
on_enter?: string[];
on_exit?: string[];
}
export interface WorkflowTransition {
action: string;
target: string;
conditions?: string[];
roles?: string[];
}
// --- Create Definition ---
export interface CreateWorkflowDefinitionDto {
/** รหัสของ Workflow (เช่น 'RFA', 'CORRESPONDENCE') */
workflow_code: string;
/** นิยาม Workflow (DSL JSON Object) */
dsl: any;
dsl: WorkflowDsl;
/** เปิดใช้งานทันทีหรือไม่ (Default: true) */
is_active?: boolean;
}
// --- Update Definition ---
export interface UpdateWorkflowDefinitionDto extends Partial<CreateWorkflowDefinitionDto> {}
export type UpdateWorkflowDefinitionDto = Partial<CreateWorkflowDefinitionDto>;
// --- Evaluate (ประมวลผล/ตรวจสอบ State) ---
export interface EvaluateWorkflowDto {
@@ -27,7 +50,7 @@ export interface EvaluateWorkflowDto {
action: string;
/** Context ข้อมูลเพิ่มเติม (เช่น User ID, Data) */
context?: Record<string, any>;
context?: Record<string, unknown>;
}
// --- Get Available Actions ---
@@ -37,4 +60,4 @@ export interface GetAvailableActionsDto {
/** สถานะปัจจุบัน */
current_state: string;
}
}

View File

@@ -60,6 +60,6 @@ export interface CreateRFADto {
dueDate?: string; // [New]
description?: string;
documentDate?: string;
details?: Record<string, any>;
details?: Record<string, unknown>;
shopDrawingRevisionIds?: number[];
}