From e0867cb59fbe549908756f332410bf7368c161ef Mon Sep 17 00:00:00 2001 From: Khafid Prayoga Date: Thu, 9 Oct 2025 11:38:42 +0700 Subject: [PATCH] wip: paginated regencies --- app/composables/useRegencies.ts | 334 +++++++++++++++++++++++++------- 1 file changed, 268 insertions(+), 66 deletions(-) diff --git a/app/composables/useRegencies.ts b/app/composables/useRegencies.ts index 9ea233f8..e480998d 100644 --- a/app/composables/useRegencies.ts +++ b/app/composables/useRegencies.ts @@ -2,38 +2,117 @@ import { ref, computed, watch } from 'vue' import { refDebounced } from '@vueuse/core' import type { Regency } from '~/models/regency' import type { SelectItem } from '~/components/pub/my-ui/form/select.vue' +import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type' import { toTitleCase } from '~/lib/utils' import * as regencyService from '~/services/regency.service' -// Global cache untuk regencies berdasarkan province code -const regenciesCache = ref>(new Map()) +// Interface untuk query parameters +interface RegencyQueryParams { + 'province-code'?: string + search?: string + 'page-number'?: number + 'page-size'?: number + sort?: string +} + +// Interface untuk cached data dengan pagination +interface CachedRegencyData { + data: Regency[] + meta: PaginationMeta + queryKey: string // untuk tracking query yang berbeda +} + +// Global cache untuk regencies berdasarkan query key (province-code + search + pagination) +const regenciesCache = ref>(new Map()) const loadingStates = ref>(new Map()) const errorStates = ref>(new Map()) -export function useRegencies(provinceCode: Ref | string | undefined) { +interface UseRegenciesOptions { + provinceCode?: Ref | string | undefined + pageSize?: number + enablePagination?: boolean + enableSearch?: boolean +} + +export function useRegencies(options: UseRegenciesOptions | Ref | string | undefined = {}) { + // Backward compatibility - jika parameter pertama adalah provinceCode + const normalizedOptions: UseRegenciesOptions = typeof options === 'object' && 'value' in options + ? { provinceCode: options } + : typeof options === 'string' || options === undefined + ? { provinceCode: options } + : options + + const { + provinceCode, + pageSize = 10, + enablePagination = true, + enableSearch = true + } = normalizedOptions + // Convert provinceCode ke ref jika bukan ref const provinceCodeRef = - typeof provinceCode === 'string' || provinceCode === undefined ? ref(provinceCode) : provinceCode + typeof provinceCode === 'string' || provinceCode === undefined ? ref(provinceCode) : provinceCode || ref(undefined) - // Computed untuk mendapatkan regencies berdasarkan province code + // State untuk pagination dan search + const currentPage = ref(1) + const currentPageSize = ref(pageSize) + const searchQuery = ref('') + const debouncedSearch = refDebounced(searchQuery, 300) + + // Function untuk generate query key + const generateQueryKey = (params: RegencyQueryParams) => { + const keyParts = [ + params['province-code'] || '', + params.search || '', + params['page-number'] || 1, + params['page-size'] || pageSize, + params.sort || 'name:asc' + ] + return keyParts.join('|') + } + + // Current query key + const currentQueryKey = computed(() => { + return generateQueryKey({ + 'province-code': provinceCodeRef.value, + search: enableSearch ? debouncedSearch.value : undefined, + 'page-number': enablePagination ? currentPage.value : undefined, + 'page-size': enablePagination ? currentPageSize.value : undefined, + sort: 'name:asc' + }) + }) + + // Computed untuk mendapatkan regencies berdasarkan current query const regencies = computed(() => { - const code = provinceCodeRef.value - if (!code) return [] - return regenciesCache.value.get(code) || [] + const queryKey = currentQueryKey.value + const cachedData = regenciesCache.value.get(queryKey) + return cachedData?.data || [] + }) + + // Computed untuk pagination meta + const paginationMeta = computed(() => { + const queryKey = currentQueryKey.value + const cachedData = regenciesCache.value.get(queryKey) + return cachedData?.meta || { + recordCount: 0, + page: currentPage.value, + pageSize: currentPageSize.value, + totalPage: 0, + hasNext: false, + hasPrev: false, + } }) // Computed untuk loading state const isLoading = computed(() => { - const code = provinceCodeRef.value - if (!code) return false - return loadingStates.value.get(code) || false + const queryKey = currentQueryKey.value + return loadingStates.value.get(queryKey) || false }) // Computed untuk error state const error = computed(() => { - const code = provinceCodeRef.value - if (!code) return null - return errorStates.value.get(code) || null + const queryKey = currentQueryKey.value + return errorStates.value.get(queryKey) || null }) // Computed untuk format SelectItem @@ -45,81 +124,166 @@ export function useRegencies(provinceCode: Ref | string | un })) }) - // Function untuk fetch regencies berdasarkan province code - async function fetchRegencies(provinceCodeParam?: string, forceRefresh = false, isUserAction = false) { - const code = provinceCodeParam || provinceCodeRef.value - if (!code) return + // Function untuk fetch regencies dengan pagination dan search + async function fetchRegencies(params?: Partial, forceRefresh = false) { + const queryParams: RegencyQueryParams = { + 'province-code': params?.['province-code'] || provinceCodeRef.value, + search: enableSearch ? (params?.search || debouncedSearch.value) : undefined, + 'page-number': enablePagination ? (params?.['page-number'] || currentPage.value) : undefined, + 'page-size': enablePagination ? (params?.['page-size'] || currentPageSize.value) : undefined, + sort: params?.sort || 'name:asc' + } - // Jika user action atau force refresh, selalu fetch - // Jika bukan user action dan sudah ada cache, skip - if (!isUserAction && !forceRefresh && regenciesCache.value.has(code)) { + // Jika tidak ada province code, return + if (!queryParams['province-code']) return + + const queryKey = generateQueryKey(queryParams) + + // Jika tidak force refresh dan sudah ada cache, skip + if (!forceRefresh && regenciesCache.value.has(queryKey)) { return } // Jika sedang loading, skip untuk mencegah duplicate calls - if (loadingStates.value.get(code)) { + if (loadingStates.value.get(queryKey)) { return } - // Tambahan: Cek apakah ada pending request untuk code yang sama - const pendingKey = `pending_${code}` - if (loadingStates.value.get(pendingKey)) { - return - } - - loadingStates.value.set(pendingKey, true) - - loadingStates.value.set(code, true) - errorStates.value.set(code, null) + loadingStates.value.set(queryKey, true) + errorStates.value.set(queryKey, null) try { - const response = await regencyService.getList({ - sort: 'name:asc', - 'page-no-limit': true, - 'province-code': code, - }) + // Prepare API parameters + const apiParams: any = { + sort: queryParams.sort, + 'province-code': queryParams['province-code'], + } + + // Add pagination or no-limit based on enablePagination + if (enablePagination && queryParams['page-number'] && queryParams['page-size']) { + apiParams['page-number'] = queryParams['page-number'] + apiParams['page-size'] = queryParams['page-size'] + } else { + apiParams['page-no-limit'] = true + } + + // Add search if enabled and has value + if (enableSearch && queryParams.search && queryParams.search.trim().length >= 3) { + apiParams.search = queryParams.search.trim() + } + + const response = await regencyService.getList(apiParams) if (response.success) { const regenciesData = response.body.data || [] - regenciesCache.value.set(code, regenciesData) + const meta = response.body.meta || { record_totalCount: regenciesData.length } + + // Create pagination meta + const paginationMeta: PaginationMeta = { + recordCount: meta.record_totalCount, + page: queryParams['page-number'] || 1, + pageSize: queryParams['page-size'] || regenciesData.length, + totalPage: enablePagination && queryParams['page-size'] + ? Math.ceil(meta.record_totalCount / queryParams['page-size']) + : 1, + hasNext: false, + hasPrev: false, + } + + if (enablePagination && queryParams['page-size']) { + paginationMeta.hasNext = paginationMeta.page < paginationMeta.totalPage + paginationMeta.hasPrev = paginationMeta.page > 1 + } + + // Cache the result + regenciesCache.value.set(queryKey, { + data: regenciesData, + meta: paginationMeta, + queryKey + }) } else { - errorStates.value.set(code, 'Gagal memuat data kabupaten/kota') + errorStates.value.set(queryKey, 'Gagal memuat data kabupaten/kota') console.error('Failed to fetch regencies:', response) } } catch (err) { - errorStates.value.set(code, 'Terjadi kesalahan saat memuat data kabupaten/kota') + errorStates.value.set(queryKey, 'Terjadi kesalahan saat memuat data kabupaten/kota') console.error('Error fetching regencies:', err) } finally { - loadingStates.value.set(code, false) - loadingStates.value.delete(pendingKey) + loadingStates.value.set(queryKey, false) } } - // Function untuk mencari regency berdasarkan code + // Function untuk mencari regency berdasarkan code (search di semua cached data) function getRegencyByCode(code: string): Regency | undefined { - const provinceCode = provinceCodeRef.value - if (!provinceCode) return undefined - - const regenciesForProvince = regenciesCache.value.get(provinceCode) || [] - return regenciesForProvince.find((regency) => regency.code === code) + // Search di semua cached data + for (const cachedData of regenciesCache.value.values()) { + const found = cachedData.data.find((regency) => regency.code === code) + if (found) return found + } + return undefined } - // Function untuk mencari regency berdasarkan name + // Function untuk mencari regency berdasarkan name (search di semua cached data) function getRegencyByName(name: string): Regency | undefined { - const provinceCode = provinceCodeRef.value - if (!provinceCode) return undefined - - const regenciesForProvince = regenciesCache.value.get(provinceCode) || [] - return regenciesForProvince.find((regency) => regency.name.toLowerCase() === name.toLowerCase()) + // Search di semua cached data + for (const cachedData of regenciesCache.value.values()) { + const found = cachedData.data.find((regency) => regency.name.toLowerCase() === name.toLowerCase()) + if (found) return found + } + return undefined } - // Function untuk clear cache province tertentu + // Function untuk pagination + function goToPage(page: number) { + if (page >= 1 && page <= paginationMeta.value.totalPage) { + currentPage.value = page + } + } + + function nextPage() { + if (paginationMeta.value.hasNext) { + currentPage.value += 1 + } + } + + function prevPage() { + if (paginationMeta.value.hasPrev) { + currentPage.value -= 1 + } + } + + function changePageSize(size: number) { + currentPageSize.value = size + currentPage.value = 1 // Reset ke halaman pertama + } + + // Function untuk search + function setSearch(query: string) { + searchQuery.value = query + currentPage.value = 1 // Reset ke halaman pertama saat search + } + + function clearSearch() { + searchQuery.value = '' + currentPage.value = 1 + } + + // Function untuk clear cache berdasarkan query key pattern function clearCache(provinceCodeParam?: string) { const code = provinceCodeParam || provinceCodeRef.value if (code) { - regenciesCache.value.delete(code) - loadingStates.value.delete(code) - errorStates.value.delete(code) + // Clear semua cache yang mengandung province code tersebut + const keysToDelete: string[] = [] + for (const [key] of regenciesCache.value.entries()) { + if (key.startsWith(code + '|')) { + keysToDelete.push(key) + } + } + keysToDelete.forEach(key => { + regenciesCache.value.delete(key) + loadingStates.value.delete(key) + errorStates.value.delete(key) + }) } } @@ -131,11 +295,8 @@ export function useRegencies(provinceCode: Ref | string | un } // Function untuk refresh data - function refreshRegencies(provinceCodeParam?: string) { - const code = provinceCodeParam || provinceCodeRef.value - if (code) { - return fetchRegencies(code, true) - } + function refreshRegencies(params?: Partial) { + return fetchRegencies(params, true) } // Debounced province code untuk mencegah multiple calls @@ -146,14 +307,37 @@ export function useRegencies(provinceCode: Ref | string | un debouncedProvinceCode, (newCode, oldCode) => { if (newCode && newCode !== oldCode) { - // Jika ada oldCode berarti user action (ganti pilihan) - const isUserAction = !!oldCode - fetchRegencies(newCode, false, isUserAction) + // Reset pagination dan search saat province code berubah + currentPage.value = 1 + if (enableSearch) { + searchQuery.value = '' + } + fetchRegencies() } }, { immediate: true }, ) + // Watch perubahan search query untuk auto fetch + watch( + debouncedSearch, + (newSearch, oldSearch) => { + if (enableSearch && newSearch !== oldSearch) { + fetchRegencies() + } + } + ) + + // Watch perubahan pagination untuk auto fetch + watch( + [currentPage, currentPageSize], + () => { + if (enablePagination) { + fetchRegencies() + } + } + ) + return { // Data regencies: readonly(regencies), @@ -162,6 +346,14 @@ export function useRegencies(provinceCode: Ref | string | un // State isLoading: readonly(isLoading), error: readonly(error), + paginationMeta: readonly(paginationMeta), + + // Search state + searchQuery: enableSearch ? searchQuery : readonly(ref('')), + + // Pagination state + currentPage: enablePagination ? currentPage : readonly(ref(1)), + currentPageSize: enablePagination ? currentPageSize : readonly(ref(pageSize)), // Methods fetchRegencies, @@ -170,6 +362,16 @@ export function useRegencies(provinceCode: Ref | string | un getRegencyByName, clearCache, clearAllCache, + + // Pagination methods + goToPage: enablePagination ? goToPage : () => {}, + nextPage: enablePagination ? nextPage : () => {}, + prevPage: enablePagination ? prevPage : () => {}, + changePageSize: enablePagination ? changePageSize : () => {}, + + // Search methods + setSearch: enableSearch ? setSearch : () => {}, + clearSearch: enableSearch ? clearSearch : () => {}, } }