'use client' import React, { forwardRef, Suspense, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react' import dynamic from 'next/dynamic' import qs from 'qs' import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query' import { usePathname, useRouter } from 'next/navigation' import type { Selection, SortDescriptor } from '@/types/heroui' import useDisclosure from '@/hooks/useDisclosure' import { addToast } from '@/lib/toast' import { TableSkeleton } from '@/components/feedback/LoadingState' import { downloadExcel, isEmptyObject } from '@/helpers' import { type ApiWithParams, type PaginationListColumnType } from '@/types' import useQueryParams from '@/hooks/useQueryParams' import { adminKeys } from '@/queries/admin/adminKeys' import PaginatedListToolbar from '@/components/paginated-list/PaginatedListToolbar' import PaginatedListPagination, { ROWS_PER_PAGE_OPTIONS } from '@/components/paginated-list/PaginatedListPagination' import PaginatedListTable from '@/components/paginated-list/PaginatedListTable' import { buildListParams, type PaginatedListRow } from '@/components/paginated-list/paginatedListUtils' import { usePaginatedListFilters } from '@/components/paginated-list/usePaginatedListFilters' import { fetchPaginatedList, fetchPaginatedListExport } from '@/components/paginated-list/paginatedListApi' import { handleServiceError } from '@/services/errorHandler' const PaginatedListFilterModal = dynamic(() => import('@/components/paginated-list/PaginatedListFilterModal'), { ssr: false, }) type ChildrenType = Record React.ReactNode> export interface PaginatedListHandle { exelReport: (fileName: string, newUrl: string) => Promise filters: Record openFilterModal: () => void refresh: () => void } interface PaginatedListProps { bulkActions?: React.ReactNode | ((selectedKeys: Selection) => React.ReactNode) columns: PaginationListColumnType[] staticData?: PaginatedListRow[] selectionMode?: 'single' | 'multiple' | 'none' url?: string itemsKey?: string children?: ChildrenType | never[] urlParams?: ApiWithParams hasDynamicButton?: boolean dynamicButtonText?: string onDynamicButtonClick?: () => void dynamicTopSection?: React.ReactNode backBtnUrl?: string filterPrefix?: string tableWrapperClass?: string } const PaginatedList = forwardRef( ( { columns, bulkActions, staticData = [], selectionMode = 'none', url, itemsKey, children = {}, urlParams = { page: 1, pageSize: 20, sort: '', filters: {}, }, hasDynamicButton, dynamicButtonText = 'افزودن', onDynamicButtonClick, dynamicTopSection, backBtnUrl, filterPrefix, tableWrapperClass = 'table-max-height p-0', }: PaginatedListProps, ref ) => { type DataType = PaginatedListRow const { isOpen, onOpenChange, onOpen, onClose } = useDisclosure() const router = useRouter() const pathname = usePathname() const queryParams = useQueryParams() const queryClient = useQueryClient() const [hasHydrated, setHasHydrated] = useState(false) const [isExporting, setIsExporting] = useState(false) const { clearFilters: handleClearFilter, filters, getFilters, removeFilter: handleRemoveFilter, setFilter: handleSetFilter, setFilters, setSelectFilter, } = usePaginatedListFilters() const [selectedKeys, setSelectedKeys] = useState(new Set([])) const [page, setPage] = useState(urlParams.page || 1) const [rowsPerPage, setRowsPerPage] = useState(urlParams.pageSize || Number(ROWS_PER_PAGE_OPTIONS[0].code)) const [sortString, setSortString] = useState('') const [sortDescriptor, setSortDescriptor] = useState({ column: 'createdAt', direction: 'ascending', }) // استفاده از useRef برای تشخیص اولین اجرا const isFirstRender = useRef(true) const preparingFilter = (resultType?: number) => buildListParams({ filterPrefix, // Read from ref so Enter/commit values are included before React re-renders. filters: getFilters(), page, resultType, rowsPerPage, sort: sortString, urlParams, }) const fetchParams = preparingFilter() const listQuery = useQuery({ queryKey: adminKeys.list(url ?? '', fetchParams), queryFn: async ({ signal }) => { try { if (!url) throw new Error('url is required') return await fetchPaginatedList({ itemsKey, page, params: fetchParams, rowsPerPage, signal, url, }) } catch (error: unknown) { if (error instanceof Error && (error.name === 'CanceledError' || error.name === 'AbortError')) throw error throw handleServiceError(error, { errorMode: 'silent' }) } }, enabled: Boolean(url) && hasHydrated, placeholderData: keepPreviousData, }) const data: PaginatedListRow[] = url ? (listQuery.data?.items ?? []) : staticData const totalItems = url ? (listQuery.data?.pagination.totalItems ?? 0) : staticData.length const isLoading = (url ? listQuery.isLoading : false) || isExporting const loadError = url && listQuery.error ? listQuery.error.message : null // Exposing the function to parent or other components useImperativeHandle(ref, () => ({ exelReport, filters, openFilterModal: onOpen, refresh: () => { if (url) void queryClient.invalidateQueries({ queryKey: adminKeys.listByUrl(url) }) }, })) useEffect(() => { if (!isEmptyObject(queryParams)) { const { page: queryPage, pageSize: queryPageSize, sort } = queryParams if (queryPage) { setPage(Number(queryPage)) } if (queryPageSize) { setRowsPerPage(Number(queryPageSize)) } if (sort) { const direction = sort.startsWith('-') ? 'descending' : 'ascending' const column = sort.startsWith('-') ? sort.slice(1) : sort setSortDescriptor({ column, direction, }) } const filtersArray = Object.keys(queryParams)?.filter((key) => key?.startsWith('filters')) if (filtersArray?.length) { filtersArray.map((item) => { const value = queryParams[item] const key = /\[(.*?)\]/.exec(item)?.[1] if (key) { if (value.includes(',')) { setFilters((prev) => ({ ...prev, [key]: { from: value.split(',')[0], to: value.split(',')[1], }, })) } else { setFilters((prev) => ({ ...prev, [key]: value, })) } } }) } } setHasHydrated(true) // URL state is intentionally hydrated once, then the query below is // allowed to run (see hasHydrated/enabled). Subscribing to queryParams // here would form a loop with the URL-sync effect further down. // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { if (!listQuery.data) return if (listQuery.data.pagination.page !== page) setPage(listQuery.data.pagination.page) if (listQuery.data.pagination.pageSize !== rowsPerPage) setRowsPerPage(listQuery.data.pagination.pageSize) // Mirrors the server's (possibly clamped/normalized) page/pageSize back // into state, same as the old handleFetch's post-response sync. // eslint-disable-next-line react-hooks/exhaustive-deps }, [listQuery.data]) useEffect(() => { if (listQuery.error) { addToast({ title: listQuery.error.message, color: 'danger' }) } }, [listQuery.error]) useEffect(() => { if (!hasHydrated || !url) return const filteredQueryParams = Object.fromEntries( Object.entries(queryParams).filter(([key]) => !(key in fetchParams) && !key.startsWith('filters')) ) const urlWithFetchParams = pathname?.split('?')[0] + '?' + qs.stringify({ ...filteredQueryParams, ...fetchParams }, { arrayFormat: 'brackets' }) router.replace(urlWithFetchParams, { scroll: false }) // These values are the explicit request triggers; queryParams/pathname/ // router are read fresh but shouldn't themselves re-trigger this sync. // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasHydrated, url, page, rowsPerPage, sortString, filters]) useEffect(() => { if (!listQuery.dataUpdatedAt) return undefined const timer = setTimeout(() => { const scrollableDiv = document.querySelector('.table-wrapper') if (scrollableDiv) { scrollableDiv.scrollTo({ top: 0, behavior: 'smooth', }) } else { window.scrollTo({ top: 0, behavior: 'smooth', }) } }, 250) return () => { clearTimeout(timer) } }, [listQuery.dataUpdatedAt]) const handleSort = () => { const sortStringTemp = sortDescriptor.direction === 'ascending' ? sortDescriptor.column : `-${sortDescriptor.column}` setSortString(sortStringTemp as string) } const pages = Math.ceil(totalItems / rowsPerPage) useEffect(() => { // چک کردن اینکه آیا اولین اجرا است یا نه if (isFirstRender.current) { isFirstRender.current = false return // از اجرای درخواست در بار اول جلوگیری می‌کند } handleSort() // sortDescriptor is the explicit sort trigger. // eslint-disable-next-line react-hooks/exhaustive-deps }, [sortDescriptor]) const renderCell = useCallback( (item: DataType, columnKey: React.Key) => { const cellValue = item[columnKey as keyof DataType] // Ensure children is of type ChildrenType before using it if (children && typeof children === 'object' && !(children instanceof Array) && (columnKey as string) in children) { const columnKeyString = columnKey as keyof typeof children return children[columnKeyString](item, cellValue) } return cellValue as React.ReactNode }, [children] ) const onNextPage = useCallback(() => { if (page < pages) { setPage(page + 1) } }, [page, pages]) const onPreviousPage = useCallback(() => { if (page > 1) { setPage(page - 1) } }, [page]) const onRowsPerPageChange = useCallback((nextRowsPerPage: number) => { setRowsPerPage(nextRowsPerPage) setPage(1) }, []) const runFilters = () => { setPage(1) onClose() } const exelReport = async (fileName: string, newUrl: string) => { try { setIsExporting(true) const params = preparingFilter(1) const exportUrl = newUrl || url if (!exportUrl) return const fileData = await fetchPaginatedListExport(exportUrl, params) downloadExcel(fileData, fileName) } finally { setIsExporting(false) } } const topContent = ( column.filterable)} selectedKeys={selectedKeys} selectionMode={selectionMode} onBack={(href) => { router.push(href) }} onClearSelection={() => { setSelectedKeys(new Set()) }} onDynamicButtonClick={onDynamicButtonClick} onOpenFilters={onOpen} /> ) const bottomContent = ( ) return ( <> !column.hideInTable)} data={data} filters={filters} isLoading={isLoading} loadError={loadError} selectedKeys={selectedKeys} selectionMode={selectionMode} sortDescriptor={sortDescriptor} sortString={sortString} tableWrapperClass={tableWrapperClass} topContent={topContent} onRemoveFilter={handleRemoveFilter} onRenderCell={renderCell} onRetry={() => void listQuery.refetch()} onRunFilters={runFilters} onSelectedKeysChange={setSelectedKeys} onSetFilter={handleSetFilter} onSetSelectFilter={setSelectFilter} onSortChange={setSortDescriptor} /> {isOpen ? ( ) : null} ) } ) PaginatedList.displayName = 'PaginatedList' const PaginatedListWrapper = forwardRef((props: PaginatedListProps, ref) => { const columnCount = props.columns?.length || 4 return ( {props.columns?.length ? (
{props.columns.map((column) => (

{column.label}

))}
) : null} } >
) }) PaginatedListWrapper.displayName = 'PaginatedListWrapper' export default PaginatedListWrapper