Merge branch 'dev' into feat/consultation-82
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import type { District } from '~/models/district'
|
||||
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
|
||||
import { toTitleCase } from '~/lib/utils'
|
||||
import * as districtService from '~/services/district.service'
|
||||
|
||||
// Global cache untuk districts berdasarkan regency code
|
||||
const districtsCache = ref<Map<string, District[]>>(new Map())
|
||||
const loadingStates = ref<Map<string, boolean>>(new Map())
|
||||
const errorStates = ref<Map<string, string | null>>(new Map())
|
||||
|
||||
export function useDistricts(regencyCode: Ref<string | undefined> | string | undefined) {
|
||||
// Convert regencyCode ke ref jika bukan ref
|
||||
const regencyCodeRef = typeof regencyCode === 'string' || regencyCode === undefined ? ref(regencyCode) : regencyCode
|
||||
|
||||
// Computed untuk mendapatkan districts berdasarkan regency code
|
||||
const districts = computed(() => {
|
||||
const code = regencyCodeRef.value
|
||||
if (!code) return []
|
||||
return districtsCache.value.get(code) || []
|
||||
})
|
||||
|
||||
// Computed untuk loading state
|
||||
const isLoading = computed(() => {
|
||||
const code = regencyCodeRef.value
|
||||
if (!code) return false
|
||||
return loadingStates.value.get(code) || false
|
||||
})
|
||||
|
||||
// Computed untuk error state
|
||||
const error = computed(() => {
|
||||
const code = regencyCodeRef.value
|
||||
if (!code) return null
|
||||
return errorStates.value.get(code) || null
|
||||
})
|
||||
|
||||
// Computed untuk format SelectItem
|
||||
const districtOptions = computed<SelectItem[]>(() => {
|
||||
return districts.value.map((district) => ({
|
||||
label: toTitleCase(district.name),
|
||||
value: district.code,
|
||||
searchValue: `${district.code} ${district.name}`.trim(),
|
||||
}))
|
||||
})
|
||||
|
||||
// Function untuk fetch districts berdasarkan regency code
|
||||
async function fetchDistricts(regencyCodeParam?: string, forceRefresh = false, isUserAction = false) {
|
||||
const code = regencyCodeParam || regencyCodeRef.value
|
||||
if (!code) return
|
||||
|
||||
// Jika user action atau force refresh, selalu fetch
|
||||
// Jika bukan user action dan sudah ada cache, skip
|
||||
if (!isUserAction && !forceRefresh && districtsCache.value.has(code)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Jika sedang loading, skip untuk mencegah duplicate calls
|
||||
if (loadingStates.value.get(code)) {
|
||||
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)
|
||||
|
||||
try {
|
||||
const response = await districtService.getList({
|
||||
sort: 'name:asc',
|
||||
'regency-code': code,
|
||||
'page-no-limit': true,
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
const districtsData = response.body.data || []
|
||||
districtsCache.value.set(code, districtsData)
|
||||
} else {
|
||||
errorStates.value.set(code, 'Gagal memuat data kecamatan')
|
||||
console.error('Failed to fetch districts:', response)
|
||||
}
|
||||
} catch (err) {
|
||||
errorStates.value.set(code, 'Terjadi kesalahan saat memuat data kecamatan')
|
||||
console.error('Error fetching districts:', err)
|
||||
} finally {
|
||||
loadingStates.value.set(code, false)
|
||||
loadingStates.value.delete(pendingKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Function untuk mencari district berdasarkan code
|
||||
function getDistrictByCode(code: string): District | undefined {
|
||||
const regencyCode = regencyCodeRef.value
|
||||
if (!regencyCode) return undefined
|
||||
|
||||
const districtsForRegency = districtsCache.value.get(regencyCode) || []
|
||||
return districtsForRegency.find((district) => district.code === code)
|
||||
}
|
||||
|
||||
// Function untuk mencari district berdasarkan name
|
||||
function getDistrictByName(name: string): District | undefined {
|
||||
const regencyCode = regencyCodeRef.value
|
||||
if (!regencyCode) return undefined
|
||||
|
||||
const districtsForRegency = districtsCache.value.get(regencyCode) || []
|
||||
return districtsForRegency.find((district) => district.name.toLowerCase() === name.toLowerCase())
|
||||
}
|
||||
|
||||
// Function untuk clear cache regency tertentu
|
||||
function clearCache(regencyCodeParam?: string) {
|
||||
const code = regencyCodeParam || regencyCodeRef.value
|
||||
if (code) {
|
||||
districtsCache.value.delete(code)
|
||||
loadingStates.value.delete(code)
|
||||
errorStates.value.delete(code)
|
||||
}
|
||||
}
|
||||
|
||||
// Function untuk clear semua cache
|
||||
function clearAllCache() {
|
||||
districtsCache.value.clear()
|
||||
loadingStates.value.clear()
|
||||
errorStates.value.clear()
|
||||
}
|
||||
|
||||
// Function untuk refresh data
|
||||
function refreshDistricts(regencyCodeParam?: string) {
|
||||
const code = regencyCodeParam || regencyCodeRef.value
|
||||
if (code) {
|
||||
return fetchDistricts(code, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Debounced regency code untuk mencegah multiple calls
|
||||
const debouncedRegencyCode = refDebounced(regencyCodeRef, 100)
|
||||
|
||||
// Watch perubahan regency code untuk auto fetch
|
||||
watch(
|
||||
debouncedRegencyCode,
|
||||
(newCode, oldCode) => {
|
||||
if (newCode && newCode !== oldCode) {
|
||||
// Jika ada oldCode berarti user action (ganti pilihan)
|
||||
const isUserAction = !!oldCode
|
||||
fetchDistricts(newCode, false, isUserAction)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return {
|
||||
// Data
|
||||
districts: readonly(districts),
|
||||
districtOptions,
|
||||
|
||||
// State
|
||||
isLoading: readonly(isLoading),
|
||||
error: readonly(error),
|
||||
|
||||
// Methods
|
||||
fetchDistricts,
|
||||
refreshDistricts,
|
||||
getDistrictByCode,
|
||||
getDistrictByName,
|
||||
clearCache,
|
||||
clearAllCache,
|
||||
}
|
||||
}
|
||||
|
||||
// Export untuk direct access ke cached data (jika diperlukan)
|
||||
export const useDistrictsCache = () => ({
|
||||
districtsCache: readonly(districtsCache),
|
||||
loadingStates: readonly(loadingStates),
|
||||
errorStates: readonly(errorStates),
|
||||
})
|
||||
@@ -0,0 +1,169 @@
|
||||
import { ref, computed, watch, readonly, type Ref } from 'vue'
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import type { PostalRegion } from '~/models/postal-region'
|
||||
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
|
||||
import * as postalRegionService from '~/services/postal-region.service'
|
||||
|
||||
// Global cache untuk postal codes berdasarkan village code
|
||||
const postalRegionCache = ref<Map<string, PostalRegion[]>>(new Map())
|
||||
const loadingStates = ref<Map<string, boolean>>(new Map())
|
||||
const errorStates = ref<Map<string, string | null>>(new Map())
|
||||
|
||||
export function usePostalRegion(villageCode: Ref<string | undefined> | string | undefined) {
|
||||
// Convert villageCode ke ref jika bukan ref
|
||||
const villageCodeRef = typeof villageCode === 'string' || villageCode === undefined ? ref(villageCode) : villageCode
|
||||
|
||||
// Computed untuk mendapatkan postalRegion berdasarkan village code
|
||||
const postalRegion = computed(() => {
|
||||
const code = villageCodeRef.value
|
||||
if (!code) return []
|
||||
return postalRegionCache.value.get(code) || []
|
||||
})
|
||||
|
||||
// Computed untuk loading state
|
||||
const isLoading = computed(() => {
|
||||
const code = villageCodeRef.value
|
||||
if (!code) return false
|
||||
return loadingStates.value.get(code) || false
|
||||
})
|
||||
|
||||
// Computed untuk error state
|
||||
const error = computed(() => {
|
||||
const code = villageCodeRef.value
|
||||
if (!code) return null
|
||||
return errorStates.value.get(code) || null
|
||||
})
|
||||
|
||||
// Computed untuk format SelectItem
|
||||
const postalRegionOptions = computed<SelectItem[]>(() => {
|
||||
return postalRegion.value.map((postalRegion) => ({
|
||||
label: postalRegion.code,
|
||||
value: postalRegion.code,
|
||||
searchValue: postalRegion.code,
|
||||
}))
|
||||
})
|
||||
|
||||
// Function untuk fetch postalRegion berdasarkan village code
|
||||
async function fetchpostalRegion(villageCodeParam?: string, forceRefresh = false, isUserAction = false) {
|
||||
const code = villageCodeParam || villageCodeRef.value
|
||||
if (!code) return
|
||||
|
||||
// Jika user action atau force refresh, selalu fetch
|
||||
// Jika bukan user action dan sudah ada cache, skip
|
||||
if (!isUserAction && !forceRefresh && postalRegionCache.value.has(code)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Jika sedang loading, skip untuk mencegah duplicate calls
|
||||
if (loadingStates.value.get(code)) {
|
||||
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)
|
||||
|
||||
try {
|
||||
const response = await postalRegionService.getList({
|
||||
sort: 'code:asc',
|
||||
'village-code': code,
|
||||
'page-no-limit': true,
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
const postalRegionData = response.body.data || []
|
||||
postalRegionCache.value.set(code, postalRegionData)
|
||||
} else {
|
||||
errorStates.value.set(code, 'Gagal memuat data kode pos')
|
||||
console.error('Failed to fetch postal codes:', response)
|
||||
}
|
||||
} catch (err) {
|
||||
errorStates.value.set(code, 'Terjadi kesalahan saat memuat data kode pos')
|
||||
console.error('Error fetching postal codes:', err)
|
||||
} finally {
|
||||
loadingStates.value.set(code, false)
|
||||
loadingStates.value.delete(pendingKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Function untuk mencari postalRegion berdasarkan code
|
||||
function getpostalRegionByCode(code: string): PostalRegion | undefined {
|
||||
const villageCode = villageCodeRef.value
|
||||
if (!villageCode) return undefined
|
||||
|
||||
const postalRegionForVillage = postalRegionCache.value.get(villageCode) || []
|
||||
return postalRegionForVillage.find((postalRegion) => postalRegion.code === code)
|
||||
}
|
||||
|
||||
// Function untuk clear cache village tertentu
|
||||
function clearCache(villageCodeParam?: string) {
|
||||
const code = villageCodeParam || villageCodeRef.value
|
||||
if (code) {
|
||||
postalRegionCache.value.delete(code)
|
||||
loadingStates.value.delete(code)
|
||||
errorStates.value.delete(code)
|
||||
}
|
||||
}
|
||||
|
||||
// Function untuk clear semua cache
|
||||
function clearAllCache() {
|
||||
postalRegionCache.value.clear()
|
||||
loadingStates.value.clear()
|
||||
errorStates.value.clear()
|
||||
}
|
||||
|
||||
// Function untuk refresh data
|
||||
function refreshpostalRegion(villageCodeParam?: string) {
|
||||
const code = villageCodeParam || villageCodeRef.value
|
||||
if (code) {
|
||||
return fetchpostalRegion(code, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Debounced village code untuk mencegah multiple calls
|
||||
const debouncedVillageCode = refDebounced(villageCodeRef, 100)
|
||||
|
||||
// Watch perubahan village code untuk auto fetch
|
||||
watch(
|
||||
debouncedVillageCode,
|
||||
(newCode, oldCode) => {
|
||||
if (newCode && newCode !== oldCode) {
|
||||
// Jika ada oldCode berarti user action (ganti pilihan)
|
||||
const isUserAction = !!oldCode
|
||||
fetchpostalRegion(newCode, false, isUserAction)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return {
|
||||
// Data
|
||||
postalRegion: readonly(postalRegion),
|
||||
postalRegionOptions,
|
||||
|
||||
// State
|
||||
isLoading: readonly(isLoading),
|
||||
error: readonly(error),
|
||||
|
||||
// Methods
|
||||
fetchpostalRegion,
|
||||
refreshpostalRegion,
|
||||
getpostalRegionByCode,
|
||||
clearCache,
|
||||
clearAllCache,
|
||||
}
|
||||
}
|
||||
|
||||
// Export untuk direct access ke cached data (jika diperlukan)
|
||||
export const usepostalRegionCache = () => ({
|
||||
postalRegionCache: readonly(postalRegionCache),
|
||||
loadingStates: readonly(loadingStates),
|
||||
errorStates: readonly(errorStates),
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Province } from '~/models/province'
|
||||
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
|
||||
import { toTitleCase } from '~/lib/utils'
|
||||
import * as provinceService from '~/services/province.service'
|
||||
|
||||
// Global state untuk caching
|
||||
const provincesCache = ref<Province[]>([])
|
||||
const isLoading = ref(false)
|
||||
const isInitialized = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
export function useProvinces() {
|
||||
// Computed untuk format SelectItem
|
||||
const provinceOptions = computed<SelectItem[]>(() => {
|
||||
return provincesCache.value.map((province) => ({
|
||||
label: toTitleCase(province.name),
|
||||
value: province.code,
|
||||
// code: province.code,
|
||||
searchValue: `${province.code} ${province.name}`.trim(), // Untuk search internal combobox
|
||||
}))
|
||||
})
|
||||
|
||||
// Function untuk fetch data provinces
|
||||
async function fetchProvinces(forceRefresh = false) {
|
||||
// Jika sudah ada data dan tidak force refresh, skip
|
||||
if (isInitialized.value && !forceRefresh) {
|
||||
return
|
||||
}
|
||||
|
||||
// Jika sedang loading, skip untuk mencegah duplicate calls
|
||||
if (isLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await provinceService.getList({
|
||||
'page-no-limit': true,
|
||||
sort: 'name:asc',
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
provincesCache.value = response.body.data || []
|
||||
isInitialized.value = true
|
||||
} else {
|
||||
error.value = 'Gagal memuat data provinsi'
|
||||
console.error('Failed to fetch provinces:', response)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = 'Terjadi kesalahan saat memuat data provinsi'
|
||||
console.error('Error fetching provinces:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Function untuk mencari province berdasarkan code
|
||||
function getProvinceByCode(code: string): Province | undefined {
|
||||
return provincesCache.value.find((province) => province.code === code)
|
||||
}
|
||||
|
||||
// Function untuk mencari province berdasarkan name
|
||||
function getProvinceByName(name: string): Province | undefined {
|
||||
return provincesCache.value.find((province) => province.name.toLowerCase() === name.toLowerCase())
|
||||
}
|
||||
|
||||
// Function untuk clear cache (jika diperlukan)
|
||||
function clearCache() {
|
||||
provincesCache.value = []
|
||||
isInitialized.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
// Function untuk refresh data
|
||||
function refreshProvinces() {
|
||||
return fetchProvinces(true)
|
||||
}
|
||||
|
||||
// Auto fetch saat composable pertama kali digunakan
|
||||
if (!isInitialized.value && !isLoading.value) {
|
||||
fetchProvinces()
|
||||
}
|
||||
|
||||
return {
|
||||
// Data
|
||||
provinces: readonly(provincesCache),
|
||||
provinceOptions,
|
||||
|
||||
// State
|
||||
isLoading: readonly(isLoading),
|
||||
isInitialized: readonly(isInitialized),
|
||||
error: readonly(error),
|
||||
|
||||
// Methods
|
||||
fetchProvinces,
|
||||
refreshProvinces,
|
||||
getProvinceByCode,
|
||||
getProvinceByName,
|
||||
clearCache,
|
||||
}
|
||||
}
|
||||
|
||||
// Export untuk direct access ke cached data (jika diperlukan)
|
||||
export const useProvincesCache = () => ({
|
||||
provinces: readonly(provincesCache),
|
||||
isLoading: readonly(isLoading),
|
||||
isInitialized: readonly(isInitialized),
|
||||
})
|
||||
@@ -0,0 +1,386 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import type { Regency } from '~/models/regency'
|
||||
import type { Item } from '~/components/pub/my-ui/combobox'
|
||||
import type { PaginationMeta } from '~/components/pub/my-ui/pagination/pagination.type'
|
||||
import { toTitleCase } from '~/lib/utils'
|
||||
import * as regencyService from '~/services/regency.service'
|
||||
|
||||
// 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 = reactive(new Map<string, CachedRegencyData>())
|
||||
const loadingStates = reactive(new Map<string, boolean>())
|
||||
const errorStates = reactive(new Map<string, string | null>())
|
||||
|
||||
interface UseRegenciesOptions {
|
||||
provinceCode?: Ref<string | undefined> | string | undefined
|
||||
pageSize?: number
|
||||
enablePagination?: boolean
|
||||
enableSearch?: boolean
|
||||
}
|
||||
|
||||
export function useRegencies(options: UseRegenciesOptions | Ref<string | undefined> | 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 || ref(undefined)
|
||||
|
||||
// State untuk pagination dan search
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(pageSize)
|
||||
const searchQuery = ref('')
|
||||
|
||||
// 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 ? searchQuery.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 queryKey = currentQueryKey.value
|
||||
const cachedData = regenciesCache.get(queryKey)
|
||||
return cachedData?.data || []
|
||||
})
|
||||
|
||||
// Computed untuk pagination meta
|
||||
const paginationMeta = computed(() => {
|
||||
const queryKey = currentQueryKey.value
|
||||
const cachedData = regenciesCache.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 queryKey = currentQueryKey.value
|
||||
return loadingStates.get(queryKey) || false
|
||||
})
|
||||
|
||||
// Computed untuk error state
|
||||
const error = computed(() => {
|
||||
const queryKey = currentQueryKey.value
|
||||
return errorStates.get(queryKey) || null
|
||||
})
|
||||
|
||||
// Computed untuk format Item
|
||||
const regencyOptions = computed<Item[]>(() => {
|
||||
return regencies.value.map((regency) => ({
|
||||
label: toTitleCase(regency.name),
|
||||
value: regency.code,
|
||||
searchValue: `${regency.code} ${regency.name}`.trim(),
|
||||
}))
|
||||
})
|
||||
|
||||
// Function untuk fetch regencies dengan pagination dan search
|
||||
async function fetchRegencies(params?: Partial<RegencyQueryParams>, forceRefresh = false) {
|
||||
const queryParams: RegencyQueryParams = {
|
||||
'province-code': params?.['province-code'] || provinceCodeRef.value,
|
||||
search: enableSearch ? params?.search || searchQuery.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 tidak ada province code, return
|
||||
// if (!queryParams['province-code']) return // buat komponen select birth
|
||||
|
||||
const queryKey = generateQueryKey(queryParams)
|
||||
|
||||
// Jika tidak force refresh dan sudah ada cache, skip
|
||||
if (!forceRefresh && regenciesCache.has(queryKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Jika sedang loading, skip untuk mencegah duplicate calls
|
||||
if (loadingStates.get(queryKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
loadingStates.set(queryKey, true)
|
||||
errorStates.set(queryKey, null)
|
||||
|
||||
try {
|
||||
// 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 || []
|
||||
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.set(queryKey, {
|
||||
data: [...regenciesData],
|
||||
meta: { ...paginationMeta },
|
||||
queryKey,
|
||||
})
|
||||
} else {
|
||||
errorStates.set(queryKey, 'Gagal memuat data kabupaten/kota')
|
||||
console.error('Failed to fetch regencies:', response)
|
||||
}
|
||||
} catch (err) {
|
||||
errorStates.set(queryKey, 'Terjadi kesalahan saat memuat data kabupaten/kota')
|
||||
console.error('Error fetching regencies:', err)
|
||||
} finally {
|
||||
loadingStates.set(queryKey, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Function untuk mencari regency berdasarkan code (search di semua cached data)
|
||||
function getRegencyByCode(code: string): Regency | undefined {
|
||||
// Search di semua cached data
|
||||
for (const cachedData of regenciesCache.values()) {
|
||||
const found = cachedData.data.find((regency) => regency.code === code)
|
||||
if (found) return found
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Function untuk mencari regency berdasarkan name (search di semua cached data)
|
||||
function getRegencyByName(name: string): Regency | undefined {
|
||||
// Search di semua cached data
|
||||
for (const cachedData of regenciesCache.values()) {
|
||||
const found = cachedData.data.find((regency) => regency.name.toLowerCase() === name.toLowerCase())
|
||||
if (found) return found
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Clear semua cache yang mengandung province code tersebut
|
||||
const keysToDelete: string[] = []
|
||||
for (const [key] of regenciesCache.entries()) {
|
||||
if (key.startsWith(code + '|')) {
|
||||
keysToDelete.push(key)
|
||||
}
|
||||
}
|
||||
keysToDelete.forEach((key) => {
|
||||
regenciesCache.delete(key)
|
||||
loadingStates.delete(key)
|
||||
errorStates.delete(key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Function untuk clear semua cache
|
||||
function clearAllCache() {
|
||||
regenciesCache.clear()
|
||||
loadingStates.clear()
|
||||
errorStates.clear()
|
||||
}
|
||||
|
||||
// Function untuk refresh data
|
||||
function refreshRegencies(params?: Partial<RegencyQueryParams>) {
|
||||
return fetchRegencies(params, true)
|
||||
}
|
||||
|
||||
// Debounced province code untuk mencegah multiple calls
|
||||
const debouncedProvinceCode = refDebounced(provinceCodeRef, 100)
|
||||
|
||||
// Watch perubahan province code untuk auto fetch
|
||||
watch(
|
||||
debouncedProvinceCode,
|
||||
(newCode, oldCode) => {
|
||||
if (newCode && newCode !== oldCode) {
|
||||
// Reset pagination dan search saat province code berubah
|
||||
currentPage.value = 1
|
||||
if (enableSearch) {
|
||||
searchQuery.value = ''
|
||||
}
|
||||
fetchRegencies()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const triggerFetchAfterIdle = useDebounceFn(() => {
|
||||
if (enableSearch) {
|
||||
currentPage.value = 1
|
||||
fetchRegencies()
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
// Watch perubahan search query untuk auto fetch dan reset halaman
|
||||
watch(searchQuery, (newSearch, oldSearch) => {
|
||||
if (newSearch !== oldSearch) {
|
||||
triggerFetchAfterIdle()
|
||||
}
|
||||
})
|
||||
|
||||
// Watch perubahan pagination untuk auto fetch
|
||||
watch([currentPage, currentPageSize], () => {
|
||||
if (enablePagination) {
|
||||
fetchRegencies()
|
||||
}
|
||||
})
|
||||
|
||||
watch(regencyOptions, (val) => {
|
||||
console.log('[regencyOptions] updated', val.length)
|
||||
})
|
||||
|
||||
return {
|
||||
// Data
|
||||
regencies: readonly(regencies),
|
||||
regencyOptions,
|
||||
|
||||
// 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,
|
||||
refreshRegencies,
|
||||
getRegencyByCode,
|
||||
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 : () => {},
|
||||
}
|
||||
}
|
||||
|
||||
// Export untuk direct access ke cached data (jika diperlukan)
|
||||
export const useRegenciesCache = () => ({
|
||||
regenciesCache: readonly(regenciesCache),
|
||||
loadingStates: readonly(loadingStates),
|
||||
errorStates: readonly(errorStates),
|
||||
})
|
||||
@@ -0,0 +1,181 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import type { Village } from '~/models/village'
|
||||
import type { SelectItem } from '~/components/pub/my-ui/form/select.vue'
|
||||
import { toTitleCase } from '~/lib/utils'
|
||||
import * as villageService from '~/services/village.service'
|
||||
|
||||
// Global cache untuk villages berdasarkan district code
|
||||
const villagesCache = ref<Map<string, Village[]>>(new Map())
|
||||
const loadingStates = ref<Map<string, boolean>>(new Map())
|
||||
const errorStates = ref<Map<string, string | null>>(new Map())
|
||||
|
||||
export function useVillages(districtCode: Ref<string | undefined> | string | undefined) {
|
||||
// Convert districtCode ke ref jika bukan ref
|
||||
const districtCodeRef =
|
||||
typeof districtCode === 'string' || districtCode === undefined ? ref(districtCode) : districtCode
|
||||
|
||||
// Computed untuk mendapatkan villages berdasarkan district code
|
||||
const villages = computed(() => {
|
||||
const code = districtCodeRef.value
|
||||
if (!code) return []
|
||||
return villagesCache.value.get(code) || []
|
||||
})
|
||||
|
||||
// Computed untuk loading state
|
||||
const isLoading = computed(() => {
|
||||
const code = districtCodeRef.value
|
||||
if (!code) return false
|
||||
return loadingStates.value.get(code) || false
|
||||
})
|
||||
|
||||
// Computed untuk error state
|
||||
const error = computed(() => {
|
||||
const code = districtCodeRef.value
|
||||
if (!code) return null
|
||||
return errorStates.value.get(code) || null
|
||||
})
|
||||
|
||||
// Computed untuk format SelectItem
|
||||
const villageOptions = computed<SelectItem[]>(() => {
|
||||
return villages.value.map((village) => ({
|
||||
label: toTitleCase(village.name),
|
||||
value: village.code,
|
||||
searchValue: `${village.code} ${village.name}`.trim(),
|
||||
}))
|
||||
})
|
||||
|
||||
// Function untuk fetch villages berdasarkan district code
|
||||
async function fetchVillages(districtCodeParam?: string, forceRefresh = false, isUserAction = false) {
|
||||
const code = districtCodeParam || districtCodeRef.value
|
||||
if (!code) return
|
||||
|
||||
// Jika user action atau force refresh, selalu fetch
|
||||
// Jika bukan user action dan sudah ada cache, skip
|
||||
if (!isUserAction && !forceRefresh && villagesCache.value.has(code)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Jika sedang loading, skip untuk mencegah duplicate calls
|
||||
if (loadingStates.value.get(code)) {
|
||||
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)
|
||||
|
||||
try {
|
||||
const response = await villageService.getList({
|
||||
sort: 'name:asc',
|
||||
'district-code': code,
|
||||
'page-no-limit': true,
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
const villagesData = response.body.data || []
|
||||
villagesCache.value.set(code, villagesData)
|
||||
} else {
|
||||
errorStates.value.set(code, 'Gagal memuat data kelurahan')
|
||||
console.error('Failed to fetch villages:', response)
|
||||
}
|
||||
} catch (err) {
|
||||
errorStates.value.set(code, 'Terjadi kesalahan saat memuat data kelurahan')
|
||||
console.error('Error fetching villages:', err)
|
||||
} finally {
|
||||
loadingStates.value.set(code, false)
|
||||
loadingStates.value.delete(pendingKey)
|
||||
}
|
||||
}
|
||||
|
||||
// Function untuk mencari village berdasarkan code
|
||||
function getVillageByCode(code: string): Village | undefined {
|
||||
const districtCode = districtCodeRef.value
|
||||
if (!districtCode) return undefined
|
||||
|
||||
const villagesForDistrict = villagesCache.value.get(districtCode) || []
|
||||
return villagesForDistrict.find((village) => village.code === code)
|
||||
}
|
||||
|
||||
// Function untuk mencari village berdasarkan name
|
||||
function getVillageByName(name: string): Village | undefined {
|
||||
const districtCode = districtCodeRef.value
|
||||
if (!districtCode) return undefined
|
||||
|
||||
const villagesForDistrict = villagesCache.value.get(districtCode) || []
|
||||
return villagesForDistrict.find((village) => village.name.toLowerCase() === name.toLowerCase())
|
||||
}
|
||||
|
||||
// Function untuk clear cache district tertentu
|
||||
function clearCache(districtCodeParam?: string) {
|
||||
const code = districtCodeParam || districtCodeRef.value
|
||||
if (code) {
|
||||
villagesCache.value.delete(code)
|
||||
loadingStates.value.delete(code)
|
||||
errorStates.value.delete(code)
|
||||
}
|
||||
}
|
||||
|
||||
// Function untuk clear semua cache
|
||||
function clearAllCache() {
|
||||
villagesCache.value.clear()
|
||||
loadingStates.value.clear()
|
||||
errorStates.value.clear()
|
||||
}
|
||||
|
||||
// Function untuk refresh data
|
||||
function refreshVillages(districtCodeParam?: string) {
|
||||
const code = districtCodeParam || districtCodeRef.value
|
||||
if (code) {
|
||||
return fetchVillages(code, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Debounced district code untuk mencegah multiple calls
|
||||
const debouncedDistrictCode = refDebounced(districtCodeRef, 100)
|
||||
|
||||
// Watch perubahan district code untuk auto fetch
|
||||
watch(
|
||||
debouncedDistrictCode,
|
||||
(newCode, oldCode) => {
|
||||
if (newCode && newCode !== oldCode) {
|
||||
// Jika ada oldCode berarti user action (ganti pilihan)
|
||||
const isUserAction = !!oldCode
|
||||
fetchVillages(newCode, false, isUserAction)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return {
|
||||
// Data
|
||||
villages: readonly(villages),
|
||||
villageOptions,
|
||||
|
||||
// State
|
||||
isLoading: readonly(isLoading),
|
||||
error: readonly(error),
|
||||
|
||||
// Methods
|
||||
fetchVillages,
|
||||
refreshVillages,
|
||||
getVillageByCode,
|
||||
getVillageByName,
|
||||
clearCache,
|
||||
clearAllCache,
|
||||
}
|
||||
}
|
||||
|
||||
// Export untuk direct access ke cached data (jika diperlukan)
|
||||
export const useVillagesCache = () => ({
|
||||
villagesCache: readonly(villagesCache),
|
||||
loadingStates: readonly(loadingStates),
|
||||
errorStates: readonly(errorStates),
|
||||
})
|
||||
Reference in New Issue
Block a user