Files
simrsx-fe/app/composables/usePaginatedList.ts
Khafid Prayoga d9a675be05 refactor(composables): extract pagination logic into usePaginatedList composable
- Remove duplicate schema.query.ts files from unit, division, and installation components
- Create new usePaginatedList composable to centralize pagination logic
- Update list.vue components to use the new composable
- Maintain same functionality while reducing code duplication
2025-09-04 16:19:51 +07:00

165 lines
4.6 KiB
TypeScript

import type { DataTableLoader } from '~/components/pub/base/data-table/type'
import type { PaginationMeta } from '~/components/pub/custom-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({
q: z.union([z.literal(''), z.string().min(3)]).optional().catch(''),
page: z.coerce.number().int().min(1).default(1).catch(1),
pageSize: z.coerce.number().int().min(5).max(20).default(10).catch(10),
})
export const defaultQueryParams = {
q: '',
page: 1,
pageSize: 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,
pageSize: params.value.pageSize,
totalPage: 0,
hasNext: false,
hasPrev: false,
})
// Search model with debounce
const searchInput = ref(params.value.q || '')
const debouncedSearch = refDebounced(searchInput, 500) // 500ms debounce
// Functions
async function fetchData() {
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
paginationMeta.pageSize = currentParams.pageSize
paginationMeta.totalPage = Math.ceil(pager.record_totalCount / paginationMeta.pageSize)
paginationMeta.hasNext = paginationMeta.page < paginationMeta.totalPage
paginationMeta.hasPrev = paginationMeta.page > 1
}
} catch (error) {
console.error(`Error fetching ${entityName} list:`, 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 = page
}
// Handle search from header component
function handleSearch(searchValue: string) {
// Update URL params - this will trigger watcher and refetch data
queryParams.q = searchValue
queryParams.page = 1 // Reset to first page when searching
}
// Watchers
// Watch for URL param changes and trigger refetch
watch(params, (newParams) => {
// Sync search input with URL params (for back/forward navigation)
if (newParams.q !== searchInput.value) {
searchInput.value = newParams.q || ''
}
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.q = newValue
queryParams.page = 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,
}
}