251211:1314 Frontend: reeactor Admin panel
This commit is contained in:
@@ -51,11 +51,13 @@ const contractSchema = z.object({
|
||||
|
||||
type ContractFormData = z.infer<typeof contractSchema>;
|
||||
|
||||
import { contractService } from "@/lib/services/contract.service";
|
||||
|
||||
// Inline hooks for simplicity, or could move to hooks/use-master-data
|
||||
const useContracts = (params?: any) => {
|
||||
return useQuery({
|
||||
queryKey: ['contracts', params],
|
||||
queryFn: () => projectService.getAllContracts(params),
|
||||
queryFn: () => contractService.getAll(params),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -19,13 +19,12 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const PROJECTS = [
|
||||
{ id: '1', name: 'LCBP3' },
|
||||
{ id: '2', name: 'LCBP3-Maintenance' },
|
||||
];
|
||||
import { useProjects } from '@/hooks/use-master-data';
|
||||
|
||||
export default function NumberingPage() {
|
||||
const { data: projects = [] } = useProjects();
|
||||
const [selectedProjectId, setSelectedProjectId] = useState("1");
|
||||
|
||||
const [templates, setTemplates] = useState<NumberingTemplate[]>([]);
|
||||
const [, setLoading] = useState(true);
|
||||
|
||||
@@ -35,7 +34,7 @@ export default function NumberingPage() {
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [testTemplate, setTestTemplate] = useState<NumberingTemplate | null>(null);
|
||||
|
||||
const selectedProjectName = PROJECTS.find(p => p.id === selectedProjectId)?.name || 'Unknown Project';
|
||||
const selectedProjectName = projects.find((p: any) => p.id.toString() === selectedProjectId)?.projectName || 'Unknown Project';
|
||||
|
||||
const loadTemplates = async () => {
|
||||
setLoading(true);
|
||||
@@ -105,9 +104,9 @@ export default function NumberingPage() {
|
||||
<SelectValue placeholder="Select Project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROJECTS.map(project => (
|
||||
<SelectItem key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
{projects.map((project: any) => (
|
||||
<SelectItem key={project.id} value={project.id.toString()}>
|
||||
{project.projectCode} - {project.projectName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -134,7 +133,7 @@ export default function NumberingPage() {
|
||||
{template.documentTypeName}
|
||||
</h3>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{PROJECTS.find(p => p.id === template.projectId?.toString())?.name || selectedProjectName}
|
||||
{projects.find((p: any) => p.id.toString() === template.projectId?.toString())?.projectName || selectedProjectName}
|
||||
</Badge>
|
||||
{template.disciplineCode && <Badge>{template.disciplineCode}</Badge>}
|
||||
<Badge variant={template.isActive ? 'default' : 'secondary'}>
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
|
||||
import { masterDataService } from "@/lib/services/master-data.service";
|
||||
import { projectService } from "@/lib/services/project.service";
|
||||
import { contractService } from "@/lib/services/contract.service";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { useState, useEffect } from "react";
|
||||
import apiClient from "@/lib/api/client";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -22,8 +21,7 @@ export default function DisciplinesPage() {
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch contracts for filter and form options
|
||||
// Fetch contracts for filter and form options
|
||||
projectService.getAllContracts().then((data) => {
|
||||
contractService.getAll().then((data) => {
|
||||
setContracts(Array.isArray(data) ? data : []);
|
||||
}).catch(err => {
|
||||
console.error("Failed to load contracts:", err);
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
import { GenericCrudTable } from "@/components/admin/reference/generic-crud-table";
|
||||
import { masterDataService } from "@/lib/services/master-data.service";
|
||||
import { projectService } from "@/lib/services/project.service";
|
||||
import { contractService } from "@/lib/services/contract.service";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { useState, useEffect } from "react";
|
||||
import apiClient from "@/lib/api/client";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -22,8 +21,7 @@ export default function RfaTypesPage() {
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch contracts for filter and form options
|
||||
// Fetch contracts for filter and form options
|
||||
projectService.getAllContracts().then((data) => {
|
||||
contractService.getAll().then((data) => {
|
||||
setContracts(Array.isArray(data) ? data : []);
|
||||
}).catch(err => {
|
||||
console.error("Failed to load contracts:", err);
|
||||
|
||||
@@ -25,7 +25,10 @@ interface Session {
|
||||
}
|
||||
|
||||
const sessionService = {
|
||||
getAll: async () => (await apiClient.get("/auth/sessions")).data,
|
||||
getAll: async () => {
|
||||
const response = await apiClient.get("/auth/sessions");
|
||||
return response.data.data || response.data;
|
||||
},
|
||||
revoke: async (sessionId: string) => (await apiClient.delete(`/auth/sessions/${sessionId}`)).data,
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ interface NumberingError {
|
||||
}
|
||||
|
||||
const logService = {
|
||||
getNumberingErrors: async () => (await apiClient.get("/document-numbering/logs/errors")).data,
|
||||
getNumberingErrors: async () => {
|
||||
const response = await apiClient.get("/document-numbering/logs/errors");
|
||||
return response.data.data || response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export default function NumberingLogsPage() {
|
||||
|
||||
@@ -7,10 +7,11 @@ import { Loader2 } from "lucide-react";
|
||||
|
||||
interface DrawingListProps {
|
||||
type: "CONTRACT" | "SHOP";
|
||||
projectId?: number;
|
||||
}
|
||||
|
||||
export function DrawingList({ type }: DrawingListProps) {
|
||||
const { data: drawings, isLoading, isError } = useDrawings(type, { projectId: 1 });
|
||||
export function DrawingList({ type, projectId }: DrawingListProps) {
|
||||
const { data: drawings, isLoading, isError } = useDrawings(type, { projectId: projectId ?? 1 });
|
||||
|
||||
// Note: The hook handles switching services based on type.
|
||||
// The params { type } might be redundant if getAll doesn't use it, but safe to pass.
|
||||
|
||||
153
frontend/components/ui/__tests__/button.test.tsx
Normal file
153
frontend/components/ui/__tests__/button.test.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { Button } from '../button';
|
||||
|
||||
describe('Button', () => {
|
||||
describe('rendering', () => {
|
||||
it('should render with default variant and size', () => {
|
||||
render(<Button>Click me</Button>);
|
||||
|
||||
const button = screen.getByRole('button', { name: /click me/i });
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveClass('bg-primary');
|
||||
expect(button).toHaveClass('h-10', 'px-4', 'py-2');
|
||||
});
|
||||
|
||||
it('should render with children text', () => {
|
||||
render(<Button>Submit Form</Button>);
|
||||
|
||||
expect(screen.getByText('Submit Form')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('variants', () => {
|
||||
it('should render destructive variant', () => {
|
||||
render(<Button variant="destructive">Delete</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('bg-destructive');
|
||||
});
|
||||
|
||||
it('should render outline variant', () => {
|
||||
render(<Button variant="outline">Cancel</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('border', 'border-input');
|
||||
});
|
||||
|
||||
it('should render secondary variant', () => {
|
||||
render(<Button variant="secondary">Secondary</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('bg-secondary');
|
||||
});
|
||||
|
||||
it('should render ghost variant', () => {
|
||||
render(<Button variant="ghost">Ghost</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('hover:bg-accent');
|
||||
});
|
||||
|
||||
it('should render link variant', () => {
|
||||
render(<Button variant="link">Link</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('underline-offset-4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sizes', () => {
|
||||
it('should render small size', () => {
|
||||
render(<Button size="sm">Small</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('h-9', 'px-3');
|
||||
});
|
||||
|
||||
it('should render large size', () => {
|
||||
render(<Button size="lg">Large</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('h-11', 'px-8');
|
||||
});
|
||||
|
||||
it('should render icon size', () => {
|
||||
render(<Button size="icon">👍</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('h-10', 'w-10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('states', () => {
|
||||
it('should be disabled when disabled prop is passed', () => {
|
||||
render(<Button disabled>Disabled</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeDisabled();
|
||||
expect(button).toHaveClass('disabled:opacity-50');
|
||||
});
|
||||
|
||||
it('should handle click events', () => {
|
||||
const handleClick = vi.fn();
|
||||
render(<Button onClick={handleClick}>Click</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not fire click when disabled', () => {
|
||||
const handleClick = vi.fn();
|
||||
render(<Button disabled onClick={handleClick}>Disabled</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(handleClick).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('asChild prop', () => {
|
||||
it('should render as child element when asChild is true', () => {
|
||||
render(
|
||||
<Button asChild>
|
||||
<a href="/test">Link Button</a>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute('href', '/test');
|
||||
expect(link).toHaveClass('bg-primary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom className', () => {
|
||||
it('should apply custom className', () => {
|
||||
render(<Button className="custom-class">Custom</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
|
||||
describe('type attribute', () => {
|
||||
it('should have type button by default', () => {
|
||||
render(<Button>Button</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
// Default type in React is undefined (browser defaults to submit in forms)
|
||||
expect(button).not.toHaveAttribute('type', 'submit');
|
||||
});
|
||||
|
||||
it('should accept submit type', () => {
|
||||
render(<Button type="submit">Submit</Button>);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveAttribute('type', 'submit');
|
||||
});
|
||||
});
|
||||
});
|
||||
270
frontend/hooks/__tests__/use-correspondence.test.ts
Normal file
270
frontend/hooks/__tests__/use-correspondence.test.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { createTestQueryClient } from '@/lib/test-utils';
|
||||
import {
|
||||
useCorrespondences,
|
||||
useCorrespondence,
|
||||
useCreateCorrespondence,
|
||||
useUpdateCorrespondence,
|
||||
useDeleteCorrespondence,
|
||||
useSubmitCorrespondence,
|
||||
useProcessWorkflow,
|
||||
correspondenceKeys,
|
||||
} from '../use-correspondence';
|
||||
import { correspondenceService } from '@/lib/services/correspondence.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Mock the service
|
||||
vi.mock('@/lib/services/correspondence.service', () => ({
|
||||
correspondenceService: {
|
||||
getAll: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
submit: vi.fn(),
|
||||
processWorkflow: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('use-correspondence hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('correspondenceKeys', () => {
|
||||
it('should generate correct cache keys', () => {
|
||||
expect(correspondenceKeys.all).toEqual(['correspondences']);
|
||||
expect(correspondenceKeys.lists()).toEqual(['correspondences', 'list']);
|
||||
expect(correspondenceKeys.list({ projectId: 1 })).toEqual([
|
||||
'correspondences',
|
||||
'list',
|
||||
{ projectId: 1 },
|
||||
]);
|
||||
expect(correspondenceKeys.details()).toEqual(['correspondences', 'detail']);
|
||||
expect(correspondenceKeys.detail(1)).toEqual(['correspondences', 'detail', 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCorrespondences', () => {
|
||||
it('should fetch correspondences successfully', async () => {
|
||||
const mockData = {
|
||||
data: [
|
||||
{ id: 1, title: 'Test Correspondence 1' },
|
||||
{ id: 2, title: 'Test Correspondence 2' },
|
||||
],
|
||||
meta: { total: 2, page: 1, limit: 10 },
|
||||
};
|
||||
|
||||
vi.mocked(correspondenceService.getAll).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCorrespondences({ projectId: 1 }), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(correspondenceService.getAll).toHaveBeenCalledWith({ projectId: 1 });
|
||||
});
|
||||
|
||||
it('should handle error state', async () => {
|
||||
const mockError = new Error('API Error');
|
||||
vi.mocked(correspondenceService.getAll).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCorrespondences({ projectId: 1 }), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCorrespondence', () => {
|
||||
it('should fetch single correspondence by id', async () => {
|
||||
const mockData = { id: 1, title: 'Test Correspondence' };
|
||||
vi.mocked(correspondenceService.getById).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCorrespondence(1), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(correspondenceService.getById).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should not fetch when id is falsy', () => {
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCorrespondence(0), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(correspondenceService.getById).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateCorrespondence', () => {
|
||||
it('should create correspondence and show success toast', async () => {
|
||||
const mockResponse = { id: 1, title: 'New Correspondence' };
|
||||
vi.mocked(correspondenceService.create).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper, queryClient } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateCorrespondence(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
title: 'New Correspondence',
|
||||
projectId: 1,
|
||||
correspondenceTypeId: 1,
|
||||
});
|
||||
});
|
||||
|
||||
expect(correspondenceService.create).toHaveBeenCalledWith({
|
||||
title: 'New Correspondence',
|
||||
projectId: 1,
|
||||
correspondenceTypeId: 1,
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith('Correspondence created successfully');
|
||||
});
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const mockError = {
|
||||
message: 'API Error',
|
||||
response: { data: { message: 'Validation failed' } },
|
||||
};
|
||||
vi.mocked(correspondenceService.create).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateCorrespondence(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
title: '',
|
||||
projectId: 1,
|
||||
correspondenceTypeId: 1,
|
||||
});
|
||||
} catch {
|
||||
// Expected to throw
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to create correspondence', {
|
||||
description: 'Validation failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateCorrespondence', () => {
|
||||
it('should update correspondence and invalidate cache', async () => {
|
||||
const mockResponse = { id: 1, title: 'Updated Correspondence' };
|
||||
vi.mocked(correspondenceService.update).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useUpdateCorrespondence(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
data: { title: 'Updated Correspondence' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(correspondenceService.update).toHaveBeenCalledWith(1, {
|
||||
title: 'Updated Correspondence',
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith('Correspondence updated successfully');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteCorrespondence', () => {
|
||||
it('should delete correspondence and show success toast', async () => {
|
||||
vi.mocked(correspondenceService.delete).mockResolvedValue({});
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDeleteCorrespondence(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync(1);
|
||||
});
|
||||
|
||||
expect(correspondenceService.delete).toHaveBeenCalledWith(1);
|
||||
expect(toast.success).toHaveBeenCalledWith('Correspondence deleted successfully');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSubmitCorrespondence', () => {
|
||||
it('should submit correspondence for workflow', async () => {
|
||||
const mockResponse = { id: 1, status: 'submitted' };
|
||||
vi.mocked(correspondenceService.submit).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useSubmitCorrespondence(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
data: { recipientIds: [2, 3] },
|
||||
});
|
||||
});
|
||||
|
||||
expect(correspondenceService.submit).toHaveBeenCalledWith(1, { recipientIds: [2, 3] });
|
||||
expect(toast.success).toHaveBeenCalledWith('Correspondence submitted successfully');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useProcessWorkflow', () => {
|
||||
it('should process workflow action', async () => {
|
||||
const mockResponse = { id: 1, status: 'approved' };
|
||||
vi.mocked(correspondenceService.processWorkflow).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useProcessWorkflow(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
data: { action: 'approve', comment: 'LGTM' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(correspondenceService.processWorkflow).toHaveBeenCalledWith(1, {
|
||||
action: 'approve',
|
||||
comment: 'LGTM',
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith('Action completed successfully');
|
||||
});
|
||||
|
||||
it('should handle workflow action error', async () => {
|
||||
const mockError = {
|
||||
message: 'Error',
|
||||
response: { data: { message: 'Permission denied' } },
|
||||
};
|
||||
vi.mocked(correspondenceService.processWorkflow).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useProcessWorkflow(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
data: { action: 'approve' },
|
||||
});
|
||||
} catch {
|
||||
// Expected to throw
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to process action', {
|
||||
description: 'Permission denied',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
212
frontend/hooks/__tests__/use-drawing.test.ts
Normal file
212
frontend/hooks/__tests__/use-drawing.test.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { createTestQueryClient } from '@/lib/test-utils';
|
||||
import {
|
||||
useDrawings,
|
||||
useDrawing,
|
||||
useCreateDrawing,
|
||||
drawingKeys,
|
||||
} from '../use-drawing';
|
||||
import { contractDrawingService } from '@/lib/services/contract-drawing.service';
|
||||
import { shopDrawingService } from '@/lib/services/shop-drawing.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Mock services
|
||||
vi.mock('@/lib/services/contract-drawing.service', () => ({
|
||||
contractDrawingService: {
|
||||
getAll: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/services/shop-drawing.service', () => ({
|
||||
shopDrawingService: {
|
||||
getAll: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('use-drawing hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('drawingKeys', () => {
|
||||
it('should generate correct cache keys', () => {
|
||||
expect(drawingKeys.all).toEqual(['drawings']);
|
||||
expect(drawingKeys.lists()).toEqual(['drawings', 'list']);
|
||||
expect(drawingKeys.list('CONTRACT', { projectId: 1 })).toEqual([
|
||||
'drawings',
|
||||
'list',
|
||||
'CONTRACT',
|
||||
{ projectId: 1 },
|
||||
]);
|
||||
expect(drawingKeys.detail('SHOP', 1)).toEqual(['drawings', 'detail', 'SHOP', 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDrawings', () => {
|
||||
it('should fetch CONTRACT drawings successfully', async () => {
|
||||
const mockData = {
|
||||
data: [
|
||||
{ id: 1, drawingNumber: 'CD-001' },
|
||||
{ id: 2, drawingNumber: 'CD-002' },
|
||||
],
|
||||
meta: { total: 2, page: 1, limit: 10 },
|
||||
};
|
||||
|
||||
vi.mocked(contractDrawingService.getAll).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDrawings('CONTRACT', { projectId: 1 }), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(contractDrawingService.getAll).toHaveBeenCalledWith({ projectId: 1 });
|
||||
expect(shopDrawingService.getAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch SHOP drawings successfully', async () => {
|
||||
const mockData = {
|
||||
data: [{ id: 1, drawingNumber: 'SD-001' }],
|
||||
meta: { total: 1, page: 1, limit: 10 },
|
||||
};
|
||||
|
||||
vi.mocked(shopDrawingService.getAll).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDrawings('SHOP', { projectId: 1 }), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(shopDrawingService.getAll).toHaveBeenCalledWith({ projectId: 1 });
|
||||
expect(contractDrawingService.getAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle error state', async () => {
|
||||
const mockError = new Error('API Error');
|
||||
vi.mocked(contractDrawingService.getAll).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDrawings('CONTRACT', { projectId: 1 }), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDrawing', () => {
|
||||
it('should fetch single CONTRACT drawing by id', async () => {
|
||||
const mockData = { id: 1, drawingNumber: 'CD-001' };
|
||||
vi.mocked(contractDrawingService.getById).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDrawing('CONTRACT', 1), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(contractDrawingService.getById).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should fetch single SHOP drawing by id', async () => {
|
||||
const mockData = { id: 1, drawingNumber: 'SD-001' };
|
||||
vi.mocked(shopDrawingService.getById).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDrawing('SHOP', 1), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(shopDrawingService.getById).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should not fetch when id is falsy', () => {
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDrawing('CONTRACT', 0), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(contractDrawingService.getById).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateDrawing', () => {
|
||||
it('should create CONTRACT drawing and show success toast', async () => {
|
||||
const mockResponse = { id: 1, drawingNumber: 'CD-001' };
|
||||
vi.mocked(contractDrawingService.create).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateDrawing('CONTRACT'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
projectId: 1,
|
||||
drawingNumber: 'CD-001',
|
||||
title: 'Test Drawing',
|
||||
});
|
||||
});
|
||||
|
||||
expect(contractDrawingService.create).toHaveBeenCalled();
|
||||
expect(toast.success).toHaveBeenCalledWith('Contract Drawing uploaded successfully');
|
||||
});
|
||||
|
||||
it('should create SHOP drawing and show success toast', async () => {
|
||||
const mockResponse = { id: 1, drawingNumber: 'SD-001' };
|
||||
vi.mocked(shopDrawingService.create).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateDrawing('SHOP'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
contractDrawingId: 1,
|
||||
title: 'Shop Drawing',
|
||||
});
|
||||
});
|
||||
|
||||
expect(shopDrawingService.create).toHaveBeenCalled();
|
||||
expect(toast.success).toHaveBeenCalledWith('Shop Drawing uploaded successfully');
|
||||
});
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const mockError = {
|
||||
message: 'API Error',
|
||||
response: { data: { message: 'File too large' } },
|
||||
};
|
||||
vi.mocked(contractDrawingService.create).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateDrawing('CONTRACT'), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
projectId: 1,
|
||||
drawingNumber: 'CD-001',
|
||||
title: 'Test',
|
||||
});
|
||||
} catch {
|
||||
// Expected to throw
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to upload drawing', {
|
||||
description: 'File too large',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
223
frontend/hooks/__tests__/use-projects.test.ts
Normal file
223
frontend/hooks/__tests__/use-projects.test.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { createTestQueryClient } from '@/lib/test-utils';
|
||||
import {
|
||||
useProjects,
|
||||
useCreateProject,
|
||||
useUpdateProject,
|
||||
useDeleteProject,
|
||||
projectKeys,
|
||||
} from '../use-projects';
|
||||
import { projectService } from '@/lib/services/project.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Mock the service
|
||||
vi.mock('@/lib/services/project.service', () => ({
|
||||
projectService: {
|
||||
getAll: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('use-projects hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('projectKeys', () => {
|
||||
it('should generate correct cache keys', () => {
|
||||
expect(projectKeys.all).toEqual(['projects']);
|
||||
expect(projectKeys.list({ search: 'test' })).toEqual([
|
||||
'projects',
|
||||
'list',
|
||||
{ search: 'test' },
|
||||
]);
|
||||
expect(projectKeys.detail(1)).toEqual(['projects', 'detail', 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useProjects', () => {
|
||||
it('should fetch projects successfully', async () => {
|
||||
const mockData = [
|
||||
{ id: 1, name: 'Project Alpha', code: 'P-001' },
|
||||
{ id: 2, name: 'Project Beta', code: 'P-002' },
|
||||
];
|
||||
|
||||
vi.mocked(projectService.getAll).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useProjects({ search: 'test' }), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(projectService.getAll).toHaveBeenCalledWith({ search: 'test' });
|
||||
});
|
||||
|
||||
it('should fetch projects without params', async () => {
|
||||
const mockData = [{ id: 1, name: 'Project Alpha' }];
|
||||
vi.mocked(projectService.getAll).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useProjects(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(projectService.getAll).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('should handle error state', async () => {
|
||||
vi.mocked(projectService.getAll).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useProjects({}), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateProject', () => {
|
||||
it('should create project and show success toast', async () => {
|
||||
const mockResponse = { id: 1, name: 'New Project' };
|
||||
vi.mocked(projectService.create).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateProject(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
name: 'New Project',
|
||||
code: 'P-003',
|
||||
contractId: 1,
|
||||
});
|
||||
});
|
||||
|
||||
expect(projectService.create).toHaveBeenCalledWith({
|
||||
name: 'New Project',
|
||||
code: 'P-003',
|
||||
contractId: 1,
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith('Project created successfully');
|
||||
});
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const mockError = {
|
||||
message: 'Error',
|
||||
response: { data: { message: 'Duplicate code' } },
|
||||
};
|
||||
vi.mocked(projectService.create).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateProject(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
name: 'Test',
|
||||
code: 'P-001',
|
||||
});
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to create project', {
|
||||
description: 'Duplicate code',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateProject', () => {
|
||||
it('should update project and show success toast', async () => {
|
||||
const mockResponse = { id: 1, name: 'Updated Project' };
|
||||
vi.mocked(projectService.update).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useUpdateProject(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
data: { name: 'Updated Project' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(projectService.update).toHaveBeenCalledWith(1, { name: 'Updated Project' });
|
||||
expect(toast.success).toHaveBeenCalledWith('Project updated successfully');
|
||||
});
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const mockError = {
|
||||
message: 'Error',
|
||||
response: { data: { message: 'Not found' } },
|
||||
};
|
||||
vi.mocked(projectService.update).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useUpdateProject(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
id: 999,
|
||||
data: { name: 'Test' },
|
||||
});
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to update project', {
|
||||
description: 'Not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteProject', () => {
|
||||
it('should delete project and show success toast', async () => {
|
||||
vi.mocked(projectService.delete).mockResolvedValue({});
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDeleteProject(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync(1);
|
||||
});
|
||||
|
||||
expect(projectService.delete).toHaveBeenCalledWith(1);
|
||||
expect(toast.success).toHaveBeenCalledWith('Project deleted successfully');
|
||||
});
|
||||
|
||||
it('should show error toast on delete failure', async () => {
|
||||
const mockError = {
|
||||
message: 'Error',
|
||||
response: { data: { message: 'Cannot delete' } },
|
||||
};
|
||||
vi.mocked(projectService.delete).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDeleteProject(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync(1);
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to delete project', {
|
||||
description: 'Cannot delete',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
215
frontend/hooks/__tests__/use-rfa.test.ts
Normal file
215
frontend/hooks/__tests__/use-rfa.test.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { createTestQueryClient } from '@/lib/test-utils';
|
||||
import {
|
||||
useRFAs,
|
||||
useRFA,
|
||||
useCreateRFA,
|
||||
useUpdateRFA,
|
||||
useProcessRFA,
|
||||
rfaKeys,
|
||||
} from '../use-rfa';
|
||||
import { rfaService } from '@/lib/services/rfa.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Mock service
|
||||
vi.mock('@/lib/services/rfa.service', () => ({
|
||||
rfaService: {
|
||||
getAll: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
processWorkflow: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('use-rfa hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rfaKeys', () => {
|
||||
it('should generate correct cache keys', () => {
|
||||
expect(rfaKeys.all).toEqual(['rfas']);
|
||||
expect(rfaKeys.lists()).toEqual(['rfas', 'list']);
|
||||
expect(rfaKeys.list({ projectId: 1 })).toEqual(['rfas', 'list', { projectId: 1 }]);
|
||||
expect(rfaKeys.details()).toEqual(['rfas', 'detail']);
|
||||
expect(rfaKeys.detail(1)).toEqual(['rfas', 'detail', 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRFAs', () => {
|
||||
it('should fetch RFAs successfully', async () => {
|
||||
const mockData = {
|
||||
data: [
|
||||
{ id: 1, rfaNumber: 'RFA-001' },
|
||||
{ id: 2, rfaNumber: 'RFA-002' },
|
||||
],
|
||||
meta: { total: 2, page: 1, limit: 10 },
|
||||
};
|
||||
|
||||
vi.mocked(rfaService.getAll).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useRFAs({ projectId: 1 }), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(rfaService.getAll).toHaveBeenCalledWith({ projectId: 1 });
|
||||
});
|
||||
|
||||
it('should handle error state', async () => {
|
||||
vi.mocked(rfaService.getAll).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useRFAs({ projectId: 1 }), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRFA', () => {
|
||||
it('should fetch single RFA by id', async () => {
|
||||
const mockData = { id: 1, rfaNumber: 'RFA-001', status: 'pending' };
|
||||
vi.mocked(rfaService.getById).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useRFA(1), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(rfaService.getById).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should not fetch when id is falsy', () => {
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useRFA(0), { wrapper });
|
||||
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
expect(rfaService.getById).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateRFA', () => {
|
||||
it('should create RFA and show success toast', async () => {
|
||||
const mockResponse = { id: 1, rfaNumber: 'RFA-001' };
|
||||
vi.mocked(rfaService.create).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateRFA(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
projectId: 1,
|
||||
subject: 'Test RFA',
|
||||
});
|
||||
});
|
||||
|
||||
expect(rfaService.create).toHaveBeenCalled();
|
||||
expect(toast.success).toHaveBeenCalledWith('RFA created successfully');
|
||||
});
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const mockError = {
|
||||
message: 'Error',
|
||||
response: { data: { message: 'Validation failed' } },
|
||||
};
|
||||
vi.mocked(rfaService.create).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateRFA(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
projectId: 1,
|
||||
subject: '',
|
||||
});
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to create RFA', {
|
||||
description: 'Validation failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateRFA', () => {
|
||||
it('should update RFA and invalidate cache', async () => {
|
||||
const mockResponse = { id: 1, subject: 'Updated RFA' };
|
||||
vi.mocked(rfaService.update).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useUpdateRFA(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
data: { subject: 'Updated RFA' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(rfaService.update).toHaveBeenCalledWith(1, { subject: 'Updated RFA' });
|
||||
expect(toast.success).toHaveBeenCalledWith('RFA updated successfully');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useProcessRFA', () => {
|
||||
it('should process workflow action and show toast', async () => {
|
||||
const mockResponse = { id: 1, status: 'approved' };
|
||||
vi.mocked(rfaService.processWorkflow).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useProcessRFA(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
data: { action: 'approve', comment: 'Approved' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(rfaService.processWorkflow).toHaveBeenCalledWith(1, {
|
||||
action: 'approve',
|
||||
comment: 'Approved',
|
||||
});
|
||||
expect(toast.success).toHaveBeenCalledWith('Workflow status updated successfully');
|
||||
});
|
||||
|
||||
it('should handle workflow error', async () => {
|
||||
const mockError = {
|
||||
message: 'Error',
|
||||
response: { data: { message: 'Permission denied' } },
|
||||
};
|
||||
vi.mocked(rfaService.processWorkflow).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useProcessRFA(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
data: { action: 'reject' },
|
||||
});
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to process workflow', {
|
||||
description: 'Permission denied',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
234
frontend/hooks/__tests__/use-users.test.ts
Normal file
234
frontend/hooks/__tests__/use-users.test.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { createTestQueryClient } from '@/lib/test-utils';
|
||||
import {
|
||||
useUsers,
|
||||
useRoles,
|
||||
useCreateUser,
|
||||
useUpdateUser,
|
||||
useDeleteUser,
|
||||
userKeys,
|
||||
} from '../use-users';
|
||||
import { userService } from '@/lib/services/user.service';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Mock the service
|
||||
vi.mock('@/lib/services/user.service', () => ({
|
||||
userService: {
|
||||
getAll: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
getRoles: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('use-users hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('userKeys', () => {
|
||||
it('should generate correct cache keys', () => {
|
||||
expect(userKeys.all).toEqual(['users']);
|
||||
expect(userKeys.list({ search: 'john' })).toEqual([
|
||||
'users',
|
||||
'list',
|
||||
{ search: 'john' },
|
||||
]);
|
||||
expect(userKeys.detail(1)).toEqual(['users', 'detail', 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUsers', () => {
|
||||
it('should fetch users successfully', async () => {
|
||||
const mockData = {
|
||||
data: [
|
||||
{ userId: 1, username: 'john', email: 'john@example.com' },
|
||||
{ userId: 2, username: 'jane', email: 'jane@example.com' },
|
||||
],
|
||||
meta: { total: 2, page: 1, limit: 10 },
|
||||
};
|
||||
|
||||
vi.mocked(userService.getAll).mockResolvedValue(mockData);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useUsers({ search: 'test' }), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockData);
|
||||
expect(userService.getAll).toHaveBeenCalledWith({ search: 'test' });
|
||||
});
|
||||
|
||||
it('should handle error state', async () => {
|
||||
vi.mocked(userService.getAll).mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useUsers(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRoles', () => {
|
||||
it('should fetch roles successfully', async () => {
|
||||
const mockRoles = [
|
||||
{ roleId: 1, name: 'Admin' },
|
||||
{ roleId: 2, name: 'Editor' },
|
||||
{ roleId: 3, name: 'Viewer' },
|
||||
];
|
||||
|
||||
vi.mocked(userService.getRoles).mockResolvedValue(mockRoles);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useRoles(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual(mockRoles);
|
||||
expect(userService.getRoles).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCreateUser', () => {
|
||||
it('should create user and show success toast', async () => {
|
||||
const mockResponse = { userId: 1, username: 'newuser' };
|
||||
vi.mocked(userService.create).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateUser(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
username: 'newuser',
|
||||
email: 'newuser@example.com',
|
||||
password: 'password123',
|
||||
roleIds: [2],
|
||||
});
|
||||
});
|
||||
|
||||
expect(userService.create).toHaveBeenCalled();
|
||||
expect(toast.success).toHaveBeenCalledWith('User created successfully');
|
||||
});
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const mockError = {
|
||||
message: 'Error',
|
||||
response: { data: { message: 'Username already exists' } },
|
||||
};
|
||||
vi.mocked(userService.create).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useCreateUser(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
username: 'existinguser',
|
||||
email: 'test@example.com',
|
||||
password: 'password',
|
||||
});
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to create user', {
|
||||
description: 'Username already exists',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateUser', () => {
|
||||
it('should update user and show success toast', async () => {
|
||||
const mockResponse = { userId: 1, email: 'updated@example.com' };
|
||||
vi.mocked(userService.update).mockResolvedValue(mockResponse);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useUpdateUser(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
id: 1,
|
||||
data: { email: 'updated@example.com' },
|
||||
});
|
||||
});
|
||||
|
||||
expect(userService.update).toHaveBeenCalledWith(1, { email: 'updated@example.com' });
|
||||
expect(toast.success).toHaveBeenCalledWith('User updated successfully');
|
||||
});
|
||||
|
||||
it('should show error toast on failure', async () => {
|
||||
const mockError = {
|
||||
message: 'Error',
|
||||
response: { data: { message: 'User not found' } },
|
||||
};
|
||||
vi.mocked(userService.update).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useUpdateUser(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync({
|
||||
id: 999,
|
||||
data: { email: 'test@example.com' },
|
||||
});
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to update user', {
|
||||
description: 'User not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteUser', () => {
|
||||
it('should delete user and show success toast', async () => {
|
||||
vi.mocked(userService.delete).mockResolvedValue({});
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDeleteUser(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync(1);
|
||||
});
|
||||
|
||||
expect(userService.delete).toHaveBeenCalledWith(1);
|
||||
expect(toast.success).toHaveBeenCalledWith('User deleted successfully');
|
||||
});
|
||||
|
||||
it('should show error toast on delete failure', async () => {
|
||||
const mockError = {
|
||||
message: 'Error',
|
||||
response: { data: { message: 'Cannot delete yourself' } },
|
||||
};
|
||||
vi.mocked(userService.delete).mockRejectedValue(mockError);
|
||||
|
||||
const { wrapper } = createTestQueryClient();
|
||||
const { result } = renderHook(() => useDeleteUser(), { wrapper });
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync(1);
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to delete user', {
|
||||
description: 'Cannot delete yourself',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,12 @@ import { correspondenceService } from '@/lib/services/correspondence.service';
|
||||
import { SearchCorrespondenceDto } from '@/types/dto/correspondence/search-correspondence.dto';
|
||||
import { CreateCorrespondenceDto } from '@/types/dto/correspondence/create-correspondence.dto';
|
||||
import { SubmitCorrespondenceDto } from '@/types/dto/correspondence/submit-correspondence.dto';
|
||||
import { WorkflowActionDto } from '@/types/dto/correspondence/workflow-action.dto';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Error type for axios errors
|
||||
type ApiError = Error & { response?: { data?: { message?: string } } };
|
||||
|
||||
// Keys for Query Cache
|
||||
export const correspondenceKeys = {
|
||||
all: ['correspondences'] as const,
|
||||
@@ -43,7 +47,7 @@ export function useCreateCorrespondence() {
|
||||
toast.success('Correspondence created successfully');
|
||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: ApiError) => {
|
||||
toast.error('Failed to create correspondence', {
|
||||
description: error.response?.data?.message || 'Something went wrong',
|
||||
});
|
||||
@@ -51,6 +55,42 @@ export function useCreateCorrespondence() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateCorrespondence() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number | string; data: Partial<CreateCorrespondenceDto> }) =>
|
||||
correspondenceService.update(id, data),
|
||||
onSuccess: (_, { id }) => {
|
||||
toast.success('Correspondence updated successfully');
|
||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.detail(id) });
|
||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
||||
},
|
||||
onError: (error: ApiError) => {
|
||||
toast.error('Failed to update correspondence', {
|
||||
description: error.response?.data?.message || 'Something went wrong',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteCorrespondence() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: number | string) => correspondenceService.delete(id),
|
||||
onSuccess: () => {
|
||||
toast.success('Correspondence deleted successfully');
|
||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
||||
},
|
||||
onError: (error: ApiError) => {
|
||||
toast.error('Failed to delete correspondence', {
|
||||
description: error.response?.data?.message || 'Something went wrong',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSubmitCorrespondence() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -62,7 +102,7 @@ export function useSubmitCorrespondence() {
|
||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.detail(id) });
|
||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: ApiError) => {
|
||||
toast.error('Failed to submit correspondence', {
|
||||
description: error.response?.data?.message || 'Something went wrong',
|
||||
});
|
||||
@@ -74,14 +114,14 @@ export function useProcessWorkflow() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number | string; data: any }) =>
|
||||
mutationFn: ({ id, data }: { id: number | string; data: WorkflowActionDto }) =>
|
||||
correspondenceService.processWorkflow(id, data),
|
||||
onSuccess: (_, { id }) => {
|
||||
toast.success('Action completed successfully');
|
||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.detail(id) });
|
||||
queryClient.invalidateQueries({ queryKey: correspondenceKeys.lists() });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: ApiError) => {
|
||||
toast.error('Failed to process action', {
|
||||
description: error.response?.data?.message || 'Something went wrong',
|
||||
});
|
||||
@@ -89,4 +129,3 @@ export function useProcessWorkflow() {
|
||||
});
|
||||
}
|
||||
|
||||
// Add more mutations as needed (update, delete, etc.)
|
||||
|
||||
@@ -6,18 +6,20 @@ import { SearchShopDrawingDto, CreateShopDrawingDto } from '@/types/dto/drawing/
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type DrawingType = 'CONTRACT' | 'SHOP';
|
||||
type DrawingSearchParams = SearchContractDrawingDto | SearchShopDrawingDto;
|
||||
type CreateDrawingData = CreateContractDrawingDto | CreateShopDrawingDto;
|
||||
|
||||
export const drawingKeys = {
|
||||
all: ['drawings'] as const,
|
||||
lists: () => [...drawingKeys.all, 'list'] as const,
|
||||
list: (type: DrawingType, params: any) => [...drawingKeys.lists(), type, params] as const,
|
||||
list: (type: DrawingType, params: DrawingSearchParams) => [...drawingKeys.lists(), type, params] as const,
|
||||
details: () => [...drawingKeys.all, 'detail'] as const,
|
||||
detail: (type: DrawingType, id: number | string) => [...drawingKeys.details(), type, id] as const,
|
||||
};
|
||||
|
||||
// --- Queries ---
|
||||
|
||||
export function useDrawings(type: DrawingType, params: any) {
|
||||
export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
|
||||
return useQuery({
|
||||
queryKey: drawingKeys.list(type, params),
|
||||
queryFn: async () => {
|
||||
@@ -51,7 +53,7 @@ export function useCreateDrawing(type: DrawingType) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data: any) => {
|
||||
mutationFn: async (data: CreateDrawingData) => {
|
||||
if (type === 'CONTRACT') {
|
||||
return contractDrawingService.create(data as CreateContractDrawingDto);
|
||||
} else {
|
||||
@@ -62,7 +64,7 @@ export function useCreateDrawing(type: DrawingType) {
|
||||
toast.success(`${type === 'CONTRACT' ? 'Contract' : 'Shop'} Drawing uploaded successfully`);
|
||||
queryClient.invalidateQueries({ queryKey: drawingKeys.lists() });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
onError: (error: Error & { response?: { data?: { message?: string } } }) => {
|
||||
toast.error('Failed to upload drawing', {
|
||||
description: error.response?.data?.message || 'Something went wrong',
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
CreateOrganizationDto,
|
||||
UpdateOrganizationDto,
|
||||
SearchOrganizationDto,
|
||||
} from '@/types/dto/organization.dto';
|
||||
} from '@/types/dto/organization/organization.dto';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
export const masterDataKeys = {
|
||||
@@ -15,10 +15,12 @@ export const masterDataKeys = {
|
||||
disciplines: (contractId?: number) => [...masterDataKeys.all, 'disciplines', contractId] as const,
|
||||
};
|
||||
|
||||
import { organizationService } from '@/lib/services/organization.service';
|
||||
|
||||
export function useOrganizations(params?: SearchOrganizationDto) {
|
||||
return useQuery({
|
||||
queryKey: [...masterDataKeys.organizations(), params],
|
||||
queryFn: () => masterDataService.getOrganizations(params),
|
||||
queryFn: () => organizationService.getAll(params),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,12 +79,23 @@ export function useDisciplines(contractId?: number) {
|
||||
});
|
||||
}
|
||||
|
||||
// Add useContracts hook
|
||||
// Add useProjects hook
|
||||
import { projectService } from '@/lib/services/project.service';
|
||||
export function useContracts(projectId: number = 1) {
|
||||
|
||||
export function useProjects(isActive: boolean = true) {
|
||||
return useQuery({
|
||||
queryKey: ['contracts', projectId],
|
||||
queryFn: () => projectService.getContracts(projectId),
|
||||
queryKey: ['projects', { isActive }],
|
||||
queryFn: () => projectService.getAll({ isActive }),
|
||||
});
|
||||
}
|
||||
|
||||
// Add useContracts hook
|
||||
import { contractService } from '@/lib/services/contract.service';
|
||||
|
||||
export function useContracts(projectId: number = 1) {
|
||||
return useQuery({
|
||||
queryKey: ['contracts', projectId],
|
||||
queryFn: () => contractService.getAll({ projectId }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
157
frontend/lib/services/__tests__/correspondence.service.test.ts
Normal file
157
frontend/lib/services/__tests__/correspondence.service.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { correspondenceService } from '../correspondence.service';
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
// apiClient is already mocked in vitest.setup.ts
|
||||
|
||||
describe('correspondenceService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should call GET /correspondences with params', async () => {
|
||||
const mockResponse = {
|
||||
data: [{ id: 1, title: 'Test' }],
|
||||
meta: { total: 1 },
|
||||
};
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await correspondenceService.getAll({ projectId: 1 });
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/correspondences', {
|
||||
params: { projectId: 1 },
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should call GET /correspondences without params', async () => {
|
||||
const mockResponse = { data: [] };
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
await correspondenceService.getAll();
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/correspondences', {
|
||||
params: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getById', () => {
|
||||
it('should call GET /correspondences/:id', async () => {
|
||||
const mockResponse = { id: 1, title: 'Test' };
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await correspondenceService.getById(1);
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/correspondences/1');
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should work with string id', async () => {
|
||||
const mockResponse = { id: 1 };
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
await correspondenceService.getById('123');
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/correspondences/123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should call POST /correspondences with data', async () => {
|
||||
const createDto = {
|
||||
title: 'New Correspondence',
|
||||
projectId: 1,
|
||||
correspondenceTypeId: 1,
|
||||
};
|
||||
const mockResponse = { id: 1, ...createDto };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await correspondenceService.create(createDto);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/correspondences', createDto);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should call PUT /correspondences/:id with data', async () => {
|
||||
const updateData = { title: 'Updated Title' };
|
||||
const mockResponse = { id: 1, title: 'Updated Title' };
|
||||
vi.mocked(apiClient.put).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await correspondenceService.update(1, updateData);
|
||||
|
||||
expect(apiClient.put).toHaveBeenCalledWith('/correspondences/1', updateData);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should call DELETE /correspondences/:id', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: {} });
|
||||
|
||||
const result = await correspondenceService.delete(1);
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/correspondences/1');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit', () => {
|
||||
it('should call POST /correspondences/:id/submit', async () => {
|
||||
const submitDto = { recipientIds: [2, 3] };
|
||||
const mockResponse = { id: 1, status: 'submitted' };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await correspondenceService.submit(1, submitDto);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/correspondences/1/submit', submitDto);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processWorkflow', () => {
|
||||
it('should call POST /correspondences/:id/workflow', async () => {
|
||||
const workflowDto = { action: 'approve', comment: 'LGTM' };
|
||||
const mockResponse = { id: 1, status: 'approved' };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await correspondenceService.processWorkflow(1, workflowDto);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/correspondences/1/workflow', workflowDto);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addReference', () => {
|
||||
it('should call POST /correspondences/:id/references', async () => {
|
||||
const referenceDto = { referencedDocumentId: 2, referenceType: 'reply_to' };
|
||||
const mockResponse = { id: 1 };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await correspondenceService.addReference(1, referenceDto);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith(
|
||||
'/correspondences/1/references',
|
||||
referenceDto
|
||||
);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeReference', () => {
|
||||
it('should call DELETE /correspondences/:id/references with body', async () => {
|
||||
const referenceDto = { referencedDocumentId: 2 };
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: {} });
|
||||
|
||||
const result = await correspondenceService.removeReference(1, referenceDto);
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/correspondences/1/references', {
|
||||
data: referenceDto,
|
||||
});
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
329
frontend/lib/services/__tests__/master-data.service.test.ts
Normal file
329
frontend/lib/services/__tests__/master-data.service.test.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { masterDataService } from '../master-data.service';
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
// apiClient is already mocked in vitest.setup.ts
|
||||
|
||||
describe('masterDataService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// --- Tags ---
|
||||
describe('Tags', () => {
|
||||
describe('getTags', () => {
|
||||
it('should call GET /tags with params', async () => {
|
||||
const mockTags = [{ id: 1, name: 'Important' }];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { data: mockTags } });
|
||||
|
||||
const result = await masterDataService.getTags({ search: 'test' });
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/master/tags', { params: { search: 'test' } });
|
||||
expect(result).toEqual(mockTags);
|
||||
});
|
||||
|
||||
it('should handle unwrapped response', async () => {
|
||||
const mockTags = [{ id: 1, name: 'Urgent' }];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockTags });
|
||||
|
||||
const result = await masterDataService.getTags();
|
||||
|
||||
expect(result).toEqual(mockTags);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTag', () => {
|
||||
it('should call POST /tags', async () => {
|
||||
const createDto = { name: 'New Tag', color: '#ff0000' };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1, ...createDto } });
|
||||
|
||||
const result = await masterDataService.createTag(createDto);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/master/tags', createDto);
|
||||
expect(result).toEqual({ id: 1, ...createDto });
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTag', () => {
|
||||
it('should call PUT /tags/:id', async () => {
|
||||
const updateDto = { name: 'Updated Tag' };
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1, ...updateDto } });
|
||||
|
||||
const result = await masterDataService.updateTag(1, updateDto);
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/master/tags/1', updateDto);
|
||||
expect(result).toEqual({ id: 1, ...updateDto });
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTag', () => {
|
||||
it('should call DELETE /tags/:id', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: {} });
|
||||
|
||||
const result = await masterDataService.deleteTag(1);
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/master/tags/1');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- Organizations ---
|
||||
describe('Organizations', () => {
|
||||
describe('getOrganizations', () => {
|
||||
it('should call GET /organizations and unwrap paginated response', async () => {
|
||||
const mockOrgs = [{ organizationId: 1, name: 'Org A' }];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { data: mockOrgs } });
|
||||
|
||||
const result = await masterDataService.getOrganizations();
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/organizations', { params: undefined });
|
||||
expect(result).toEqual(mockOrgs);
|
||||
});
|
||||
|
||||
it('should handle array response', async () => {
|
||||
const mockOrgs = [{ organizationId: 1 }];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockOrgs });
|
||||
|
||||
const result = await masterDataService.getOrganizations();
|
||||
|
||||
expect(result).toEqual(mockOrgs);
|
||||
});
|
||||
|
||||
it('should return empty array as fallback', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: {} });
|
||||
|
||||
const result = await masterDataService.getOrganizations();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createOrganization', () => {
|
||||
it('should call POST /organizations', async () => {
|
||||
const createDto = { name: 'New Org', code: 'ORG-001' };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { organizationId: 1, ...createDto } });
|
||||
|
||||
const result = await masterDataService.createOrganization(createDto);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/organizations', createDto);
|
||||
expect(result.organizationId).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOrganization', () => {
|
||||
it('should call PUT /organizations/:id', async () => {
|
||||
const updateDto = { name: 'Updated Org' };
|
||||
vi.mocked(apiClient.put).mockResolvedValue({ data: { organizationId: 1, ...updateDto } });
|
||||
|
||||
const result = await masterDataService.updateOrganization(1, updateDto);
|
||||
|
||||
expect(apiClient.put).toHaveBeenCalledWith('/organizations/1', updateDto);
|
||||
expect(result.name).toBe('Updated Org');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOrganization', () => {
|
||||
it('should call DELETE /organizations/:id', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: {} });
|
||||
|
||||
await masterDataService.deleteOrganization(1);
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/organizations/1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- Disciplines ---
|
||||
describe('Disciplines', () => {
|
||||
describe('getDisciplines', () => {
|
||||
it('should call GET /master/disciplines with contractId', async () => {
|
||||
const mockDisciplines = [{ id: 1, name: 'Civil' }];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { data: mockDisciplines } });
|
||||
|
||||
const result = await masterDataService.getDisciplines(1);
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/master/disciplines', {
|
||||
params: { contractId: 1 },
|
||||
});
|
||||
expect(result).toEqual(mockDisciplines);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDiscipline', () => {
|
||||
it('should call POST /master/disciplines', async () => {
|
||||
const createDto = { name: 'Electrical', contractId: 1 };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1, ...createDto } });
|
||||
|
||||
const result = await masterDataService.createDiscipline(createDto);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/master/disciplines', createDto);
|
||||
expect(result.name).toBe('Electrical');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteDiscipline', () => {
|
||||
it('should call DELETE /master/disciplines/:id', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: {} });
|
||||
|
||||
await masterDataService.deleteDiscipline(1);
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/master/disciplines/1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- SubTypes ---
|
||||
describe('SubTypes', () => {
|
||||
describe('getSubTypes', () => {
|
||||
it('should call GET /master/sub-types with contractId and typeId', async () => {
|
||||
const mockSubTypes = [{ id: 1, name: 'Submittal' }];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { data: mockSubTypes } });
|
||||
|
||||
const result = await masterDataService.getSubTypes(1, 2);
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/master/sub-types', {
|
||||
params: { contractId: 1, correspondenceTypeId: 2 },
|
||||
});
|
||||
expect(result).toEqual(mockSubTypes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSubType', () => {
|
||||
it('should call POST /master/sub-types', async () => {
|
||||
const createDto = { name: 'New SubType' };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1, ...createDto } });
|
||||
|
||||
const result = await masterDataService.createSubType(createDto);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/master/sub-types', createDto);
|
||||
expect(result).toEqual({ id: 1, ...createDto });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- RFA Types ---
|
||||
describe('RfaTypes', () => {
|
||||
describe('getRfaTypes', () => {
|
||||
it('should call GET /master/rfa-types', async () => {
|
||||
const mockTypes = [{ id: 1, name: 'Material Approval' }];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { data: mockTypes } });
|
||||
|
||||
const result = await masterDataService.getRfaTypes(1);
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/master/rfa-types', {
|
||||
params: { contractId: 1 },
|
||||
});
|
||||
expect(result).toEqual(mockTypes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createRfaType', () => {
|
||||
it('should call POST /master/rfa-types', async () => {
|
||||
const data = { name: 'New RFA Type' };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1, ...data } });
|
||||
|
||||
const result = await masterDataService.createRfaType(data);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/master/rfa-types', data);
|
||||
expect(result).toEqual({ id: 1, ...data });
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRfaType', () => {
|
||||
it('should call PATCH /master/rfa-types/:id', async () => {
|
||||
const data = { name: 'Updated Type' };
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1, ...data } });
|
||||
|
||||
await masterDataService.updateRfaType(1, data);
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/master/rfa-types/1', data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteRfaType', () => {
|
||||
it('should call DELETE /master/rfa-types/:id', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: {} });
|
||||
|
||||
await masterDataService.deleteRfaType(1);
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/master/rfa-types/1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- Correspondence Types ---
|
||||
describe('CorrespondenceTypes', () => {
|
||||
describe('getCorrespondenceTypes', () => {
|
||||
it('should call GET /master/correspondence-types', async () => {
|
||||
const mockTypes = [{ id: 1, name: 'Letter' }];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { data: mockTypes } });
|
||||
|
||||
const result = await masterDataService.getCorrespondenceTypes();
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/master/correspondence-types');
|
||||
expect(result).toEqual(mockTypes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCorrespondenceType', () => {
|
||||
it('should call POST /master/correspondence-types', async () => {
|
||||
const data = { name: 'Memo' };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 2, ...data } });
|
||||
|
||||
await masterDataService.createCorrespondenceType(data);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/master/correspondence-types', data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCorrespondenceType', () => {
|
||||
it('should call PATCH /master/correspondence-types/:id', async () => {
|
||||
const data = { name: 'Updated Type' };
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1, ...data } });
|
||||
|
||||
await masterDataService.updateCorrespondenceType(1, data);
|
||||
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/master/correspondence-types/1', data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCorrespondenceType', () => {
|
||||
it('should call DELETE /master/correspondence-types/:id', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: {} });
|
||||
|
||||
await masterDataService.deleteCorrespondenceType(1);
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/master/correspondence-types/1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- Number Format ---
|
||||
describe('NumberFormat', () => {
|
||||
describe('saveNumberFormat', () => {
|
||||
it('should call POST /document-numbering/formats', async () => {
|
||||
const data = { projectId: 1, correspondenceTypeId: 1, format: '{PREFIX}-{YYYY}-{SEQ}' };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1, ...data } });
|
||||
|
||||
await masterDataService.saveNumberFormat(data);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/document-numbering/formats', data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNumberFormat', () => {
|
||||
it('should call GET /document-numbering/formats with params', async () => {
|
||||
const mockFormat = { id: 1, format: '{PREFIX}-{SEQ}' };
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockFormat });
|
||||
|
||||
const result = await masterDataService.getNumberFormat(1, 2);
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/document-numbering/formats', {
|
||||
params: { projectId: 1, correspondenceTypeId: 2 },
|
||||
});
|
||||
expect(result).toEqual(mockFormat);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
95
frontend/lib/services/__tests__/project.service.test.ts
Normal file
95
frontend/lib/services/__tests__/project.service.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { projectService } from '../project.service';
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
// apiClient is already mocked in vitest.setup.ts
|
||||
|
||||
describe('projectService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should call GET /projects with params', async () => {
|
||||
const mockData = [{ id: 1, name: 'Project Alpha' }];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockData });
|
||||
|
||||
const result = await projectService.getAll({ search: 'alpha' });
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/projects', {
|
||||
params: { search: 'alpha' },
|
||||
});
|
||||
expect(result).toEqual(mockData);
|
||||
});
|
||||
|
||||
it('should unwrap paginated response', async () => {
|
||||
const mockData = [{ id: 1, name: 'Test' }];
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: { data: mockData, meta: { total: 1 } },
|
||||
});
|
||||
|
||||
const result = await projectService.getAll();
|
||||
|
||||
expect(result).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getById', () => {
|
||||
it('should call GET /projects/:id', async () => {
|
||||
const mockResponse = { id: 1, name: 'Project Alpha', code: 'P-001' };
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await projectService.getById(1);
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/projects/1');
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should work with string id', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: {} });
|
||||
|
||||
await projectService.getById('123');
|
||||
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/projects/123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should call POST /projects with data', async () => {
|
||||
const createDto = { projectName: 'New Project', projectCode: 'P-002' };
|
||||
const mockResponse = { id: 2, ...createDto };
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await projectService.create(createDto);
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/projects', createDto);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should call PUT /projects/:id with data', async () => {
|
||||
const updateData = { projectName: 'Updated Project' };
|
||||
const mockResponse = { id: 1, projectName: 'Updated Project' };
|
||||
vi.mocked(apiClient.put).mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await projectService.update(1, updateData);
|
||||
|
||||
expect(apiClient.put).toHaveBeenCalledWith('/projects/1', updateData);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should call DELETE /projects/:id', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: {} });
|
||||
|
||||
const result = await projectService.delete(1);
|
||||
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/projects/1');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
56
frontend/lib/services/contract.service.ts
Normal file
56
frontend/lib/services/contract.service.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import apiClient from "@/lib/api/client";
|
||||
import {
|
||||
CreateContractDto,
|
||||
UpdateContractDto,
|
||||
SearchContractDto,
|
||||
} from "@/types/dto/contract/contract.dto";
|
||||
|
||||
export const contractService = {
|
||||
/**
|
||||
* Get all contracts (supports filtering by projectId)
|
||||
* GET /contracts?projectId=1
|
||||
*/
|
||||
getAll: async (params?: SearchContractDto) => {
|
||||
const response = await apiClient.get("/contracts", { params });
|
||||
if (response.data && Array.isArray(response.data.data)) {
|
||||
return response.data.data;
|
||||
}
|
||||
return response.data.data || response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get contract by ID
|
||||
* GET /contracts/:id
|
||||
*/
|
||||
getById: async (id: number) => {
|
||||
const response = await apiClient.get(`/contracts/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create new contract
|
||||
* POST /contracts
|
||||
*/
|
||||
create: async (data: CreateContractDto) => {
|
||||
const response = await apiClient.post("/contracts", data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update contract
|
||||
* PATCH /contracts/:id
|
||||
*/
|
||||
update: async (id: number, data: UpdateContractDto) => {
|
||||
const response = await apiClient.patch(`/contracts/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete contract
|
||||
* DELETE /contracts/:id
|
||||
*/
|
||||
delete: async (id: number) => {
|
||||
const response = await apiClient.delete(`/contracts/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -11,33 +11,33 @@ import {
|
||||
CreateOrganizationDto,
|
||||
UpdateOrganizationDto,
|
||||
SearchOrganizationDto,
|
||||
} from "@/types/dto/organization.dto";
|
||||
} from "@/types/dto/organization/organization.dto";
|
||||
|
||||
export const masterDataService = {
|
||||
// --- Tags Management ---
|
||||
|
||||
/** ดึงรายการ Tags ทั้งหมด (Search & Pagination) */
|
||||
getTags: async (params?: SearchTagDto) => {
|
||||
const response = await apiClient.get("/tags", { params });
|
||||
const response = await apiClient.get("/master/tags", { params });
|
||||
// Support both wrapped and unwrapped scenarios
|
||||
return response.data.data || response.data;
|
||||
},
|
||||
|
||||
/** สร้าง Tag ใหม่ */
|
||||
createTag: async (data: CreateTagDto) => {
|
||||
const response = await apiClient.post("/tags", data);
|
||||
const response = await apiClient.post("/master/tags", data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** แก้ไข Tag */
|
||||
updateTag: async (id: number | string, data: UpdateTagDto) => {
|
||||
const response = await apiClient.put(`/tags/${id}`, data);
|
||||
const response = await apiClient.patch(`/master/tags/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** ลบ Tag */
|
||||
deleteTag: async (id: number | string) => {
|
||||
const response = await apiClient.delete(`/tags/${id}`);
|
||||
const response = await apiClient.delete(`/master/tags/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
57
frontend/lib/services/organization.service.ts
Normal file
57
frontend/lib/services/organization.service.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import apiClient from "@/lib/api/client";
|
||||
import {
|
||||
CreateOrganizationDto,
|
||||
UpdateOrganizationDto,
|
||||
SearchOrganizationDto,
|
||||
} from "@/types/dto/organization/organization.dto";
|
||||
|
||||
export const organizationService = {
|
||||
/**
|
||||
* Get all organizations (supports filtering by projectId)
|
||||
* GET /organizations?projectId=1
|
||||
*/
|
||||
getAll: async (params?: SearchOrganizationDto) => {
|
||||
const response = await apiClient.get("/organizations", { params });
|
||||
// Normalize response if wrapped in data.data or direct data
|
||||
if (response.data && Array.isArray(response.data.data)) {
|
||||
return response.data.data;
|
||||
}
|
||||
return response.data.data || response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get organization by ID
|
||||
* GET /organizations/:id
|
||||
*/
|
||||
getById: async (id: number) => {
|
||||
const response = await apiClient.get(`/organizations/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create new organization
|
||||
* POST /organizations
|
||||
*/
|
||||
create: async (data: CreateOrganizationDto) => {
|
||||
const response = await apiClient.post("/organizations", data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update organization
|
||||
* PATCH /organizations/:id
|
||||
*/
|
||||
update: async (id: number, data: UpdateOrganizationDto) => {
|
||||
const response = await apiClient.patch(`/organizations/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete organization
|
||||
* DELETE /organizations/:id
|
||||
*/
|
||||
delete: async (id: number) => {
|
||||
const response = await apiClient.delete(`/organizations/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -49,39 +49,8 @@ export const projectService = {
|
||||
|
||||
// --- Related Data / Dropdown Helpers ---
|
||||
|
||||
/** * ดึงรายชื่อองค์กรในโครงการ (สำหรับ Dropdown 'To/From')
|
||||
* GET /projects/:id/organizations
|
||||
*/
|
||||
getOrganizations: async (projectId: string | number) => {
|
||||
const response = await apiClient.get(`/projects/${projectId}/organizations`);
|
||||
// Unwrap the response data if it's wrapped in a 'data' property by the interceptor
|
||||
return response.data.data || response.data;
|
||||
},
|
||||
|
||||
/** * ดึงรายชื่อสัญญาในโครงการ
|
||||
* GET /projects/:id/contracts
|
||||
*/
|
||||
/** * ดึงรายชื่อสัญญาในโครงการ (Legacy/Specific)
|
||||
* GET /projects/:id/contracts
|
||||
*/
|
||||
getContracts: async (projectId: string | number) => {
|
||||
// Note: If backend doesn't have /projects/:id/contracts, use /contracts?projectId=:id
|
||||
const response = await apiClient.get(`/contracts`, { params: { projectId } });
|
||||
// Handle paginated response
|
||||
if (response.data && Array.isArray(response.data.data)) {
|
||||
return response.data.data;
|
||||
}
|
||||
return response.data.data || response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* ดึงรายการสัญญาเรื้งหมด (Global Search)
|
||||
*/
|
||||
getAllContracts: async (params?: any) => {
|
||||
const response = await apiClient.get("/contracts", { params });
|
||||
if (response.data && Array.isArray(response.data.data)) {
|
||||
return response.data.data;
|
||||
}
|
||||
return response.data.data || response.data;
|
||||
}
|
||||
// --- Related Data / Dropdown Helpers ---
|
||||
// Organizations and Contracts should now be fetched via their respective services
|
||||
// organizationService.getAll({ projectId })
|
||||
// contractService.getAll({ projectId })
|
||||
};
|
||||
|
||||
35
frontend/lib/test-utils.tsx
Normal file
35
frontend/lib/test-utils.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
/**
|
||||
* Creates a wrapper with QueryClient for testing hooks
|
||||
* @returns Object with wrapper component and queryClient instance
|
||||
*/
|
||||
export function createTestQueryClient() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
staleTime: 0,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
return { wrapper, queryClient };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all pending operations in React Query
|
||||
*/
|
||||
export async function waitForQueryClient(queryClient: QueryClient) {
|
||||
await queryClient.getQueryCache().clear();
|
||||
await queryClient.getMutationCache().clear();
|
||||
}
|
||||
@@ -7,7 +7,10 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"format": "prettier --write ."
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest",
|
||||
"test:watch": "vitest --watch",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
@@ -52,15 +55,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.33",
|
||||
"jsdom": "^27.3.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.0.15"
|
||||
}
|
||||
}
|
||||
|
||||
17
frontend/types/dto/contract/contract.dto.ts
Normal file
17
frontend/types/dto/contract/contract.dto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface CreateContractDto {
|
||||
contractCode: string;
|
||||
contractName: string;
|
||||
projectId: number;
|
||||
description?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
export interface UpdateContractDto extends Partial<CreateContractDto> {}
|
||||
|
||||
export interface SearchContractDto {
|
||||
search?: string;
|
||||
projectId?: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export interface UpdateOrganizationDto {
|
||||
|
||||
export interface SearchOrganizationDto {
|
||||
search?: string;
|
||||
projectId?: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
26
frontend/vitest.config.ts
Normal file
26
frontend/vitest.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
include: ['hooks/**/*.test.{ts,tsx}', 'lib/**/*.test.{ts,tsx}', 'components/**/*.test.{ts,tsx}'],
|
||||
exclude: ['**/node_modules/**', '**/.ignored_node_modules/**', '**/.next/**', '**/dist/**'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
include: ['hooks/**/*.ts', 'lib/**/*.ts', 'components/**/*.tsx'],
|
||||
exclude: ['**/*.d.ts', '**/__tests__/**', '**/types/**'],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './'),
|
||||
},
|
||||
},
|
||||
});
|
||||
38
frontend/vitest.setup.ts
Normal file
38
frontend/vitest.setup.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock sonner toast
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
loading: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock next/navigation
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
back: vi.fn(),
|
||||
forward: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
prefetch: vi.fn(),
|
||||
}),
|
||||
usePathname: () => '/',
|
||||
useSearchParams: () => new URLSearchParams(),
|
||||
useParams: () => ({}),
|
||||
}));
|
||||
|
||||
// Mock apiClient
|
||||
vi.mock('@/lib/api/client', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user