import type { DataTableLoader } from '~/components/pub/my-ui/data-table/type' import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type' import { refDebounced, useUrlSearchParams } from '@vueuse/core' import * as z from 'zod' // Default query schema yang bisa digunakan semua list export const defaultQuerySchema = z.object({ search: z .union([z.literal(''), z.string().min(3)]) .optional() .catch(''), 'page-number': z.coerce.number().int().min(1).default(1).catch(1), 'page-size': z.coerce.number().int().min(5).max(20).default(10).catch(10), }) export const defaultQueryParams: Record = { search: '', 'page-number': 1, 'page-size': 10, } export type DefaultQueryParams = z.infer interface UsePaginatedListOptions { // Schema untuk validasi query parameters querySchema?: z.ZodSchema defaultQuery?: Record // Fungsi untuk fetch data fetchFn: (params: any) => Promise<{ success: boolean body: { data: T[] meta: { record_totalCount: number } } }> // Nama endpoint untuk logging error entityName: string /** * Apakah state harus disinkronkan ke URL Browser? * Set `false` jika digunakan di dalam Modal, Drawer, atau Nested Component * agar tidak menimpa URL halaman induk. * @default true */ syncToUrl?: boolean } export function usePaginatedList(options: UsePaginatedListOptions) { const { querySchema = defaultQuerySchema, defaultQuery = defaultQueryParams, fetchFn, entityName, syncToUrl = true, // Default true agar behavior lama tetap jalan } = options // State management const data = ref([]) const isLoading = reactive({ isTableLoading: false, }) let queryParams: any if (syncToUrl) { // Mode Halaman Utama: Sync ke URL queryParams = useUrlSearchParams('history', { initialValue: defaultQuery, removeFalsyValues: true, write: false, }) } else { // Mode Nested/Modal: Local Reactive State queryParams = reactive({ ...defaultQuery }) } const params = computed(() => { const result = querySchema.safeParse(queryParams) return result.data || defaultQuery }) // Pagination state - computed from URL params const paginationMeta = reactive({ recordCount: 0, page: params.value['page-number'], pageSize: params.value['page-size'], totalPage: 0, hasNext: false, hasPrev: false, }) // Search model with debounce const searchInput = ref(params.value.search || '') const debouncedSearch = refDebounced(searchInput, 500) // 500ms debounce // Functions async function fetchData() { if (isLoading.isTableLoading) return isLoading.isTableLoading = true try { // Use current params from URL state const currentParams = params.value const response = await fetchFn(currentParams) if (response.success) { const responseBody = response.body data.value = responseBody.data || [] const pager = responseBody.meta // Update pagination meta from response paginationMeta.recordCount = pager.record_totalCount paginationMeta.page = currentParams['page-number'] paginationMeta.pageSize = currentParams['page-size'] paginationMeta.totalPage = Math.ceil(pager.record_totalCount / paginationMeta.pageSize) paginationMeta.hasNext = paginationMeta.page < paginationMeta.totalPage paginationMeta.hasPrev = paginationMeta.page > 1 } } catch (error) { data.value = [] paginationMeta.recordCount = 0 paginationMeta.totalPage = 0 paginationMeta.hasNext = false paginationMeta.hasPrev = false } finally { isLoading.isTableLoading = false } } // Handle pagination page change function handlePageChange(page: number) { // Update URL params - this will trigger watcher queryParams['page-number'] = page } // Handle search from header component function handleSearch(searchValue: string) { // Update URL params - this will trigger watcher and refetch data queryParams.search = searchValue queryParams['page-number'] = 1 // Reset to first page when searching } // Watchers // Watch for URL param changes and trigger refetch watch( params, (newParams) => { console.log('watch ~ newParams', newParams) // Sync search input with URL params (for back/forward navigation) if (newParams.search !== searchInput.value) { searchInput.value = newParams.search || '' } fetchData() }, { deep: true }, ) // Watch debounced search and update URL params (keeping for backward compatibility) watch(debouncedSearch, (newValue) => { // Only search if 3+ characters or empty (to clear search) if (newValue.length === 0 || newValue.length >= 3) { queryParams.search = newValue queryParams['page-number'] = 1 // Reset to first page when searching } }) // Initialize data on mount onMounted(() => { fetchData() }) return { // State data, isLoading, paginationMeta, searchInput, params, queryParams, // Functions fetchData, handlePageChange, handleSearch, } } export function transform(endpoint: string, params: any): string { const urlParams = new URLSearchParams() Object.entries(params).forEach(([key, value]) => { if (value) { urlParams.append(key, value.toString()) } }) return `${endpoint}?${urlParams.toString()}` }