Files
simrsx-fe/app/composables/usePaginatedList.ts
2025-10-20 23:41:23 +07:00

182 lines
5.1 KiB
TypeScript

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'
import { is } from "date-fns/locale"
// 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<string, any> = {
search: '',
'page-number': 1,
'page-size': 10,
}
export type DefaultQueryParams = z.infer<typeof defaultQuerySchema>
interface UsePaginatedListOptions<T = any> {
// Schema untuk validasi query parameters
querySchema?: z.ZodSchema<any>
defaultQuery?: Record<string, any>
// Fungsi untuk fetch data
fetchFn: (params: any) => Promise<{
success: boolean
body: {
data: T[]
meta: {
record_totalCount: number
}
}
}>
// Nama endpoint untuk logging error
entityName: string
}
export function usePaginatedList<T = any>(options: UsePaginatedListOptions<T>) {
const { querySchema = defaultQuerySchema, defaultQuery = defaultQueryParams, fetchFn, entityName } = options
// State management
const data = ref<T[]>([])
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
// URL state management
const queryParams = useUrlSearchParams('history', {
initialValue: defaultQuery,
removeFalsyValues: true,
})
const params = computed(() => {
const result = querySchema.safeParse(queryParams)
return result.data || defaultQuery
})
// Pagination state - computed from URL params
const paginationMeta = reactive<PaginationMeta>({
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()}`
}