diff --git a/src/components/Common/PaginationControl/PaginationControl.test.tsx b/src/components/Common/PaginationControl/PaginationControl.test.tsx new file mode 100644 index 000000000..9f4050026 --- /dev/null +++ b/src/components/Common/PaginationControl/PaginationControl.test.tsx @@ -0,0 +1,74 @@ +import { render, screen } from '@testing-library/react' +import { describe, test, expect, vi } from 'vitest' +import { PaginationControl } from './PaginationControl' +import type { PaginationControlProps } from './PaginationControlTypes' +import { ThemeProvider } from '@/contexts/ThemeProvider' +import { ComponentsProvider } from '@/contexts/ComponentAdapter/ComponentsProvider' +import { defaultComponents } from '@/contexts/ComponentAdapter/adapters/defaultComponentAdapter' + +const basePaginationProps: PaginationControlProps = { + currentPage: 1, + totalPages: 3, + itemsPerPage: 5, + handleFirstPage: vi.fn(), + handlePreviousPage: vi.fn(), + handleNextPage: vi.fn(), + handleLastPage: vi.fn(), + handleItemsPerPageChange: vi.fn(), +} + +const renderPaginationControl = (props: Partial = {}) => { + return render( + + + + + , + ) +} + +describe('PaginationControl Visibility', () => { + describe('based on totalCount', () => { + test('hides when totalCount is 0 (empty state)', () => { + renderPaginationControl({ totalCount: 0 }) + expect(screen.queryByTestId('pagination-control')).not.toBeInTheDocument() + }) + + test('hides when totalCount <= MINIMUM_PAGE_SIZE (5)', () => { + renderPaginationControl({ totalCount: 5 }) + expect(screen.queryByTestId('pagination-control')).not.toBeInTheDocument() + }) + + test('hides when totalCount is 3 (less than min page size)', () => { + renderPaginationControl({ totalCount: 3 }) + expect(screen.queryByTestId('pagination-control')).not.toBeInTheDocument() + }) + + test('shows when totalCount > MINIMUM_PAGE_SIZE', () => { + renderPaginationControl({ totalCount: 6 }) + expect(screen.getByTestId('pagination-control')).toBeInTheDocument() + }) + + test('shows when totalCount is undefined (server info unavailable)', () => { + renderPaginationControl({ totalCount: undefined }) + expect(screen.getByTestId('pagination-control')).toBeInTheDocument() + }) + + test('shows when totalCount is large', () => { + renderPaginationControl({ totalCount: 100 }) + expect(screen.getByTestId('pagination-control')).toBeInTheDocument() + }) + }) + + describe('edge cases', () => { + test('shows pagination when totalCount > 5 even with totalPages = 1', () => { + renderPaginationControl({ totalCount: 10, totalPages: 1, itemsPerPage: 50 }) + expect(screen.getByTestId('pagination-control')).toBeInTheDocument() + }) + + test('hides when totalCount is exactly 5', () => { + renderPaginationControl({ totalCount: 5, totalPages: 1 }) + expect(screen.queryByTestId('pagination-control')).not.toBeInTheDocument() + }) + }) +}) diff --git a/src/components/Common/PaginationControl/PaginationControl.tsx b/src/components/Common/PaginationControl/PaginationControl.tsx index 1b838f3a9..5c42cb591 100644 --- a/src/components/Common/PaginationControl/PaginationControl.tsx +++ b/src/components/Common/PaginationControl/PaginationControl.tsx @@ -9,9 +9,18 @@ import PaginationPrevIcon from '@/assets/icons/pagination_previous.svg?react' import PaginationNextIcon from '@/assets/icons/pagination_next.svg?react' import PaginationLastIcon from '@/assets/icons/pagination_last.svg?react' +const MINIMUM_PAGE_SIZE = 5 + +const shouldShowPagination = (totalCount: number | undefined): boolean => { + if (totalCount === undefined) return true + if (totalCount === 0) return false + return totalCount > MINIMUM_PAGE_SIZE +} + const DefaultPaginationControl = ({ currentPage, totalPages, + totalCount, isFetching, handleFirstPage, handlePreviousPage, @@ -23,7 +32,7 @@ const DefaultPaginationControl = ({ const { t } = useTranslation('common') const Components = useComponentContext() - if (totalPages < 2) { + if (!shouldShowPagination(totalCount)) { return null } diff --git a/src/components/Common/PaginationControl/PaginationControlTypes.ts b/src/components/Common/PaginationControl/PaginationControlTypes.ts index 072dc9069..f4d366a5b 100644 --- a/src/components/Common/PaginationControl/PaginationControlTypes.ts +++ b/src/components/Common/PaginationControl/PaginationControlTypes.ts @@ -8,6 +8,7 @@ export type PaginationControlProps = { handleItemsPerPageChange: (n: PaginationItemsPerPage) => void currentPage: number totalPages: number + totalCount?: number itemsPerPage?: PaginationItemsPerPage isFetching?: boolean } diff --git a/src/components/Company/Locations/LocationsList/List.tsx b/src/components/Company/Locations/LocationsList/List.tsx index 206a45b25..9809e58f4 100644 --- a/src/components/Company/Locations/LocationsList/List.tsx +++ b/src/components/Company/Locations/LocationsList/List.tsx @@ -14,6 +14,7 @@ export const List = () => { handleEditLocation, currentPage, totalPages, + totalCount, handleFirstPage, handleItemsPerPageChange, handleLastPage, @@ -89,6 +90,7 @@ export const List = () => { handleItemsPerPageChange, currentPage, totalPages, + totalCount, itemsPerPage, }, emptyState: () => ( diff --git a/src/components/Company/Locations/LocationsList/LocationsList.tsx b/src/components/Company/Locations/LocationsList/LocationsList.tsx index e3773477e..a55f5e4f9 100644 --- a/src/components/Company/Locations/LocationsList/LocationsList.tsx +++ b/src/components/Company/Locations/LocationsList/LocationsList.tsx @@ -35,6 +35,7 @@ function Root({ companyId, className, children }: LocationsListProps) { } = useLocationsGetSuspense({ companyId, page: currentPage, per: itemsPerPage }) const totalPages = Number(httpMeta.response.headers.get('x-total-pages') ?? 1) + const totalCount = Number(httpMeta.response.headers.get('x-total-count') ?? 0) const handleItemsPerPageChange = (newCount: PaginationItemsPerPage) => { setItemsPerPage(newCount) @@ -69,6 +70,7 @@ function Root({ companyId, className, children }: LocationsListProps) { locationList: locationList ?? [], currentPage, totalPages, + totalCount, handleFirstPage, handlePreviousPage, handleNextPage, diff --git a/src/components/Company/Locations/LocationsList/useLocationsList.ts b/src/components/Company/Locations/LocationsList/useLocationsList.ts index 8710240e9..0ee18f90b 100644 --- a/src/components/Company/Locations/LocationsList/useLocationsList.ts +++ b/src/components/Company/Locations/LocationsList/useLocationsList.ts @@ -5,6 +5,7 @@ import type { PaginationItemsPerPage } from '@/components/Common/PaginationContr type LocationsListContextType = { locationList: Location[] totalPages: number + totalCount: number currentPage: number itemsPerPage: PaginationItemsPerPage handleItemsPerPageChange: (n: PaginationItemsPerPage) => void diff --git a/src/components/Contractor/ContractorList/index.tsx b/src/components/Contractor/ContractorList/index.tsx index 88ab84e28..8fa68eada 100644 --- a/src/components/Contractor/ContractorList/index.tsx +++ b/src/components/Contractor/ContractorList/index.tsx @@ -139,6 +139,7 @@ function Root({ companyId, className, dictionary, successMessage }: ContractorLi handleItemsPerPageChange, currentPage, totalPages, + totalCount, itemsPerPage, }, }) diff --git a/src/components/Employee/EmployeeList/EmployeeList.tsx b/src/components/Employee/EmployeeList/EmployeeList.tsx index 825282d81..d0f3b9f25 100644 --- a/src/components/Employee/EmployeeList/EmployeeList.tsx +++ b/src/components/Employee/EmployeeList/EmployeeList.tsx @@ -62,6 +62,7 @@ function Root({ companyId, className, children, dictionary }: EmployeeListProps) const employees = employeeList! const totalPages = Number(httpMeta.response.headers.get('x-total-pages') ?? 1) + const totalCount = Number(httpMeta.response.headers.get('x-total-count') ?? 0) const handleItemsPerPageChange = (newCount: PaginationItemsPerPage) => { setItemsPerPage(newCount) @@ -139,6 +140,7 @@ function Root({ companyId, className, children, dictionary }: EmployeeListProps) employees, currentPage, totalPages, + totalCount, handleFirstPage, handlePreviousPage, handleNextPage, diff --git a/src/components/Employee/EmployeeList/List.tsx b/src/components/Employee/EmployeeList/List.tsx index c60ee9356..32cd6ce18 100644 --- a/src/components/Employee/EmployeeList/List.tsx +++ b/src/components/Employee/EmployeeList/List.tsx @@ -27,6 +27,7 @@ export const List = () => { handleItemsPerPageChange, currentPage, totalPages, + totalCount, itemsPerPage, handleSkip, isFetching, @@ -139,6 +140,7 @@ export const List = () => { handleItemsPerPageChange, currentPage, totalPages, + totalCount, itemsPerPage, }, emptyState: () => ( diff --git a/src/components/Employee/EmployeeList/useEmployeeList.ts b/src/components/Employee/EmployeeList/useEmployeeList.ts index f7ebe30a6..3ba2ec6ac 100644 --- a/src/components/Employee/EmployeeList/useEmployeeList.ts +++ b/src/components/Employee/EmployeeList/useEmployeeList.ts @@ -18,6 +18,7 @@ type EmployeeListContextType = { handleItemsPerPageChange: (newCount: PaginationItemsPerPage) => void currentPage: number totalPages: number + totalCount: number employees: Employee[] itemsPerPage: PaginationItemsPerPage isFetching: boolean diff --git a/src/components/Payroll/PayrollConfiguration/PayrollConfiguration.tsx b/src/components/Payroll/PayrollConfiguration/PayrollConfiguration.tsx index d48636e09..9fca82796 100644 --- a/src/components/Payroll/PayrollConfiguration/PayrollConfiguration.tsx +++ b/src/components/Payroll/PayrollConfiguration/PayrollConfiguration.tsx @@ -74,6 +74,7 @@ export const Root = ({ }, [employeeData.showEmployees]) const totalPages = Number(employeeData.httpMeta.response.headers.get('x-total-pages') ?? 1) + const totalCount = Number(employeeData.httpMeta.response.headers.get('x-total-count') ?? 0) const handleItemsPerPageChange = (newCount: PaginationItemsPerPage) => { setItemsPerPage(newCount) @@ -99,6 +100,7 @@ export const Root = ({ handleLastPage, handleItemsPerPageChange, totalPages, + totalCount, isFetching: isFetchingEmployeeData, itemsPerPage, } diff --git a/src/hooks/usePagination/index.ts b/src/hooks/usePagination/index.ts deleted file mode 100644 index 0eac514e7..000000000 --- a/src/hooks/usePagination/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { usePagination, extractPaginationMeta } from './usePagination' diff --git a/src/hooks/usePagination/usePagination.test.ts b/src/hooks/usePagination/usePagination.test.ts deleted file mode 100644 index ad5565517..000000000 --- a/src/hooks/usePagination/usePagination.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { describe, test, expect } from 'vitest' -import { renderHook, act } from '@testing-library/react' -import { usePagination, extractPaginationMeta } from './usePagination' - -const createMockHttpMeta = (headers: Record = {}) => ({ - response: { - headers: new Headers(headers), - } as Response, -}) - -describe('extractPaginationMeta', () => { - test('extracts totalPages from x-total-pages header', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const result = extractPaginationMeta(httpMeta) - expect(result.totalPages).toBe(5) - }) - - test('extracts totalItems from x-total-count header', () => { - const httpMeta = createMockHttpMeta({ 'x-total-count': '42' }) - const result = extractPaginationMeta(httpMeta) - expect(result.totalItems).toBe(42) - }) - - test('defaults totalPages to 1 when header missing', () => { - const httpMeta = createMockHttpMeta({}) - const result = extractPaginationMeta(httpMeta) - expect(result.totalPages).toBe(1) - }) - - test('defaults totalItems to 0 when header missing', () => { - const httpMeta = createMockHttpMeta({}) - const result = extractPaginationMeta(httpMeta) - expect(result.totalItems).toBe(0) - }) - - test('defaults totalPages to 1 when header is invalid', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': 'invalid' }) - const result = extractPaginationMeta(httpMeta) - expect(result.totalPages).toBe(1) - }) - - test('defaults totalItems to 0 when header is invalid', () => { - const httpMeta = createMockHttpMeta({ 'x-total-count': 'abc' }) - const result = extractPaginationMeta(httpMeta) - expect(result.totalItems).toBe(0) - }) - - test('defaults totalPages to 1 when header is empty string', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '' }) - const result = extractPaginationMeta(httpMeta) - expect(result.totalPages).toBe(1) - }) -}) - -describe('usePagination', () => { - describe('initial state', () => { - test('defaults currentPage to 1', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta)) - expect(result.current.currentPage).toBe(1) - }) - - test('defaults itemsPerPage to 5', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta)) - expect(result.current.itemsPerPage).toBe(5) - }) - - test('uses initialPage from options', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta, { initialPage: 3 })) - expect(result.current.currentPage).toBe(3) - }) - - test('uses initialItemsPerPage from options', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta, { initialItemsPerPage: 50 })) - expect(result.current.itemsPerPage).toBe(50) - }) - }) - - describe('navigation handlers', () => { - test('handleFirstPage sets page to 1', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta, { initialPage: 3 })) - - act(() => { - result.current.handleFirstPage() - }) - - expect(result.current.currentPage).toBe(1) - }) - - test('handlePreviousPage decrements page', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta, { initialPage: 3 })) - - act(() => { - result.current.handlePreviousPage() - }) - - expect(result.current.currentPage).toBe(2) - }) - - test('handleNextPage increments page', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta, { initialPage: 3 })) - - act(() => { - result.current.handleNextPage() - }) - - expect(result.current.currentPage).toBe(4) - }) - - test('handleLastPage sets page to totalPages', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta)) - - act(() => { - result.current.handleLastPage() - }) - - expect(result.current.currentPage).toBe(5) - }) - - test('handleItemsPerPageChange updates itemsPerPage and resets to page 1', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta, { initialPage: 3 })) - - act(() => { - result.current.handleItemsPerPageChange(50) - }) - - expect(result.current.itemsPerPage).toBe(50) - expect(result.current.currentPage).toBe(1) - }) - }) - - describe('edge cases', () => { - test('handlePreviousPage on page 1 stays on page 1', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta, { initialPage: 1 })) - - act(() => { - result.current.handlePreviousPage() - }) - - expect(result.current.currentPage).toBe(1) - }) - - test('handleNextPage on last page stays on last page', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '5' }) - const { result } = renderHook(() => usePagination(httpMeta, { initialPage: 5 })) - - act(() => { - result.current.handleNextPage() - }) - - expect(result.current.currentPage).toBe(5) - }) - }) - - describe('header extraction', () => { - test('returns totalPages from httpMeta', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '10', 'x-total-count': '100' }) - const { result } = renderHook(() => usePagination(httpMeta)) - expect(result.current.totalPages).toBe(10) - }) - - test('returns totalItems from httpMeta', () => { - const httpMeta = createMockHttpMeta({ 'x-total-pages': '10', 'x-total-count': '100' }) - const { result } = renderHook(() => usePagination(httpMeta)) - expect(result.current.totalItems).toBe(100) - }) - }) -}) diff --git a/src/hooks/usePagination/usePagination.ts b/src/hooks/usePagination/usePagination.ts deleted file mode 100644 index 990106525..000000000 --- a/src/hooks/usePagination/usePagination.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useCallback, useMemo, useState } from 'react' -import type { PaginationItemsPerPage } from '@/components/Common/PaginationControl/PaginationControlTypes' - -type HttpMeta = { - response: Response -} - -type UsePaginationOptions = { - initialPage?: number - initialItemsPerPage?: PaginationItemsPerPage -} - -function parseHeaderInt(value: string | null, defaultValue: number): number { - if (value === null) return defaultValue - const parsed = parseInt(value, 10) - return Number.isNaN(parsed) ? defaultValue : parsed -} - -export function extractPaginationMeta(httpMeta?: HttpMeta | null) { - return { - totalPages: parseHeaderInt(httpMeta?.response.headers.get('x-total-pages') ?? null, 1), - totalItems: parseHeaderInt(httpMeta?.response.headers.get('x-total-count') ?? null, 0), - } -} - -export function usePagination(httpMeta: HttpMeta | undefined, options?: UsePaginationOptions) { - const [currentPage, setCurrentPage] = useState(options?.initialPage ?? 1) - const [itemsPerPage, setItemsPerPage] = useState( - options?.initialItemsPerPage ?? 5, - ) - - const { totalPages, totalItems } = extractPaginationMeta(httpMeta) - - const handleFirstPage = useCallback(() => { - setCurrentPage(1) - }, [setCurrentPage]) - - const handlePreviousPage = useCallback(() => { - setCurrentPage(prevPage => Math.max(prevPage - 1, 1)) - }, [setCurrentPage]) - - const handleNextPage = useCallback(() => { - setCurrentPage(prevPage => Math.min(prevPage + 1, totalPages)) - }, [setCurrentPage, totalPages]) - - const handleLastPage = useCallback(() => { - setCurrentPage(totalPages) - }, [setCurrentPage, totalPages]) - - const handleItemsPerPageChange = useCallback( - (n: PaginationItemsPerPage) => { - setItemsPerPage(n) - setCurrentPage(1) - }, - [setItemsPerPage, setCurrentPage], - ) - - return useMemo( - () => ({ - currentPage, - totalPages, - totalItems, - itemsPerPage, - handleFirstPage, - handlePreviousPage, - handleNextPage, - handleLastPage, - handleItemsPerPageChange, - }), - [ - currentPage, - totalPages, - totalItems, - itemsPerPage, - handleFirstPage, - handlePreviousPage, - handleNextPage, - handleLastPage, - handleItemsPerPageChange, - ], - ) -}