import { ref, computed, type Ref, type ComputedRef } from 'vue' import { useUrlState } from './useUrlState' import type { DataTableSort, DataTablePagination, DataTableColumnFilters, SortDirection } from '~/shared/types/dataTable' export interface UseDataTableDeps { /** Called whenever sort/page/search/perPage/filter changes. The composable does NOT fetch data itself. */ fetchData: () => void | Promise } export interface UseDataTableOptions { /** Default sort field */ defaultSort?: string /** Default sort direction */ defaultDirection?: SortDirection /** Default items per page */ defaultPerPage?: number /** Available per-page options */ perPageOptions?: number[] /** Search debounce in ms. Default: 300 */ searchDebounceMs?: number /** Whether to persist state to URL. Default: true */ persistToUrl?: boolean /** Extra URL state params for page-specific filters */ extraParams?: Record } export interface UseDataTableReturn { searchTerm: Ref sortField: Ref sortDirection: Ref currentPage: Ref itemsPerPage: Ref columnFilters: Ref filters: Record> sort: ComputedRef pagination: (total: Ref, pageItems: Ref) => ComputedRef handleSort: (newSort: DataTableSort) => void handlePageChange: (page: number) => void handlePerPageChange: (perPage: number) => void handleFilterChange: () => void handleColumnFiltersChange: (filters: DataTableColumnFilters) => void debouncedSearch: () => void refresh: () => void perPageOptions: number[] } export function useDataTable( deps: UseDataTableDeps, options: UseDataTableOptions = {}, ): UseDataTableReturn { const { defaultSort = 'name', defaultDirection = 'asc', defaultPerPage = 20, perPageOptions = [20, 50, 100], searchDebounceMs = 300, persistToUrl = true, extraParams = {}, } = options let searchTerm: Ref let sortField: Ref let sortDirection: Ref let currentPage: Ref let itemsPerPage: Ref const filters: Record> = {} if (persistToUrl) { const paramDefs: Record = { page: { default: 1, type: 'number' }, perPage: { default: defaultPerPage, type: 'number' }, q: { default: '', debounce: searchDebounceMs }, sort: { default: defaultSort }, dir: { default: defaultDirection }, ...extraParams, } const state = useUrlState(paramDefs, { onRestore: () => deps.fetchData(), }) searchTerm = state.q as Ref sortField = state.sort as Ref sortDirection = state.dir as unknown as Ref currentPage = state.page as unknown as Ref itemsPerPage = state.perPage as unknown as Ref for (const key of Object.keys(extraParams)) { filters[key] = (state as Record>)[key]! } } else { searchTerm = ref('') sortField = ref(defaultSort) sortDirection = ref(defaultDirection) as Ref currentPage = ref(1) itemsPerPage = ref(defaultPerPage) for (const [key, def] of Object.entries(extraParams)) { filters[key] = ref(def.default) } } // Search debounce let searchTimeout: ReturnType | null = null const debouncedSearch = () => { if (searchTimeout) clearTimeout(searchTimeout) searchTimeout = setTimeout(() => { currentPage.value = 1 deps.fetchData() }, searchDebounceMs) } // Sort const sort = computed(() => ({ field: sortField.value, direction: sortDirection.value, })) const handleSort = (newSort: DataTableSort) => { sortField.value = newSort.field sortDirection.value = newSort.direction currentPage.value = 1 deps.fetchData() } // Pagination const handlePageChange = (page: number) => { currentPage.value = page deps.fetchData() } const handlePerPageChange = (perPage: number) => { itemsPerPage.value = perPage currentPage.value = 1 deps.fetchData() } // Column filters const columnFilters = ref({}) const handleColumnFiltersChange = (newFilters: DataTableColumnFilters) => { columnFilters.value = newFilters currentPage.value = 1 deps.fetchData() } // Generic filter change handler (resets page and refetches) const handleFilterChange = () => { currentPage.value = 1 deps.fetchData() } const pagination = (total: Ref, pageItems: Ref): ComputedRef => computed(() => ({ currentPage: currentPage.value, totalPages: Math.ceil(total.value / itemsPerPage.value) || 1, totalItems: total.value, pageItems: pageItems.value, perPageOptions, perPage: itemsPerPage.value, })) const refresh = () => deps.fetchData() return { searchTerm, sortField, sortDirection, currentPage, itemsPerPage, columnFilters, filters, sort, pagination, handleSort, handlePageChange, handlePerPageChange, handleFilterChange, handleColumnFiltersChange, debouncedSearch, refresh, perPageOptions, } }