260224:1606 20260224:1600 V1.8.0
All checks were successful
Build and Deploy / deploy (push) Successful in 6m25s
All checks were successful
Build and Deploy / deploy (push) Successful in 6m25s
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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 ${
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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'] });
|
||||
},
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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")
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: [] }
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
20
frontend/types/api-error.ts
Normal file
20
frontend/types/api-error.ts
Normal 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;
|
||||
}
|
||||
@@ -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' }[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface CreateContractDrawingDto {
|
||||
}
|
||||
|
||||
// --- Update (Partial) ---
|
||||
export interface UpdateContractDrawingDto extends Partial<CreateContractDrawingDto> {}
|
||||
export type UpdateContractDrawingDto = Partial<CreateContractDrawingDto>;
|
||||
|
||||
// --- Search ---
|
||||
export interface SearchContractDrawingDto {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
24
frontend/types/dto/master/correspondence-type.dto.ts
Normal file
24
frontend/types/dto/master/correspondence-type.dto.ts
Normal 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;
|
||||
}
|
||||
21
frontend/types/dto/master/rfa-type.dto.ts
Normal file
21
frontend/types/dto/master/rfa-type.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface CreateRfaDto {
|
||||
}
|
||||
|
||||
// --- Update (Partial) ---
|
||||
export interface UpdateRfaDto extends Partial<CreateRfaDto> {}
|
||||
export type UpdateRfaDto = Partial<CreateRfaDto>;
|
||||
|
||||
// --- Search ---
|
||||
export interface SearchRfaDto {
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface CreateTransmittalItemDto {
|
||||
}
|
||||
|
||||
// --- Update (Partial) ---
|
||||
export interface UpdateTransmittalDto extends Partial<CreateTransmittalDto> {}
|
||||
export type UpdateTransmittalDto = Partial<CreateTransmittalDto>;
|
||||
|
||||
// --- Search ---
|
||||
export interface SearchTransmittalDto {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,6 @@ export interface CreateRFADto {
|
||||
dueDate?: string; // [New]
|
||||
description?: string;
|
||||
documentDate?: string;
|
||||
details?: Record<string, any>;
|
||||
details?: Record<string, unknown>;
|
||||
shopDrawingRevisionIds?: number[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user