调整存储的逻辑

This commit is contained in:
wintsa 2026-03-10 17:48:12 +08:00
parent bbc8777b74
commit 3ad7bae1a9
9 changed files with 285 additions and 141 deletions

View File

@ -8,7 +8,7 @@ import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { parseNumberOrNull } from '@/lib/number'
import { syncPricingTotalToZxFw, type ZxFwPricingField, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { syncPricingTotalToZxFw, type ZxFwPricingField } from '@/lib/zxFwPricingSync'
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
@ -402,7 +402,7 @@ const saveToIndexedDB = async () => {
field: props.syncField,
value: totalServiceBudget.value
})
if (synced) pricingPaneReloadStore.emit(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
if (!synced) return
}
if (props.syncMainStorageKey && props.syncRowId) {
htFeeMethodReloadStore.emit(props.syncMainStorageKey, props.syncRowId)

View File

@ -11,8 +11,7 @@ import { Pencil, Eraser } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { useTabStore } from '@/pinia/tab'
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import {
AlertDialogAction,
AlertDialogCancel,
@ -65,15 +64,6 @@ interface MethodQuantityState {
detailRows?: MethodQuantityRowLike[]
}
interface ZxFwRowLike {
id?: unknown
subtotal?: unknown
}
interface ZxFwStateLike {
detailRows?: ZxFwRowLike[]
}
interface LegacyFeeRow {
id?: string
feeItem?: string
@ -91,7 +81,7 @@ const props = defineProps<{
}>()
const tabStore = useTabStore()
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
const pricingPaneReloadStore = usePricingPaneReloadStore()
const zxFwPricingStore = useZxFwPricingStore()
const createRowId = () => `fee-method-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`
const createDefaultRow = (name = ''): FeeMethodRow => ({
@ -121,17 +111,9 @@ const loadContractServiceFeeBase = async (): Promise<number | null> => {
const contractId = String(props.contractId || '').trim()
if (!contractId) return null
try {
const data = await localforage.getItem<ZxFwStateLike>(`zxFW-${contractId}`)
const rows = Array.isArray(data?.detailRows) ? data.detailRows : []
const fixedRow = rows.find(row => String(row?.id || '') === 'fixed-budget-c')
const fixedSubtotal = toFiniteUnknown(fixedRow?.subtotal)
if (fixedSubtotal != null) return round3(fixedSubtotal)
const sum = rows.reduce((acc, row) => {
if (String(row?.id || '') === 'fixed-budget-c') return acc
const subtotal = toFiniteUnknown(row?.subtotal)
return subtotal == null ? acc : acc + subtotal
}, 0)
return round3(sum)
await zxFwPricingStore.loadContract(contractId)
const base = zxFwPricingStore.getBaseSubtotal(contractId)
return base == null ? null : round3(base)
} catch (error) {
console.error('loadContractServiceFeeBase failed:', error)
return null
@ -610,12 +592,13 @@ watch(
)
watch(
() => pricingPaneReloadStore.persistedSeq,
() => {
const contractId = String(props.contractId || '').trim()
if (!contractId) return 0
return zxFwPricingStore.contractVersions[contractId] || 0
},
(nextVersion, prevVersion) => {
if (nextVersion === prevVersion || nextVersion === 0) return
const contractId = String(props.contractId || '').trim()
if (!contractId) return
if (!matchPricingPaneReload(pricingPaneReloadStore.lastPersistedEvent, contractId, ZXFW_RELOAD_SERVICE_KEY)) return
void loadFromIndexedDB()
}
)

View File

@ -3,18 +3,8 @@ import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'v
import localforage from 'localforage'
import { parseNumberOrNull } from '@/lib/number'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { useHtFeeMethodReloadStore } from '@/pinia/htFeeMethodReload'
interface ZxFwRowLike {
id?: unknown
subtotal?: unknown
}
interface ZxFwStateLike {
detailRows?: ZxFwRowLike[]
}
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
interface RateMethodState {
rate: number | null
@ -28,18 +18,18 @@ const props = defineProps<{
syncMainStorageKey?: string
syncRowId?: string
}>()
const pricingPaneReloadStore = usePricingPaneReloadStore()
const htFeeMethodReloadStore = useHtFeeMethodReloadStore()
const zxFwPricingStore = useZxFwPricingStore()
const base = ref<number | null>(null)
const rate = ref<number | null>(null)
const remark = ref('')
const rateInput = ref('')
const toFinite = (value: unknown): number | null => {
const numeric = Number(value)
return Number.isFinite(numeric) ? numeric : null
}
const contractVersion = computed(() => {
const contractId = String(props.contractId || '').trim()
if (!contractId) return 0
return zxFwPricingStore.contractVersions[contractId] || 0
})
const round3 = (value: number) => Number(value.toFixed(3))
const budgetFee = computed<number | null>(() => {
@ -51,27 +41,15 @@ const formatAmount = (value: number | null) =>
value == null ? '' : formatThousandsFlexible(value, 3)
const loadBase = async () => {
const contractId = String(props.contractId || '').trim()
if (!contractId) {
base.value = null
return
}
try {
const data = await localforage.getItem<ZxFwStateLike>(`zxFW-${contractId}`)
const rows = Array.isArray(data?.detailRows) ? data.detailRows : []
const fixedRow = rows.find(row => String(row?.id || '') === 'fixed-budget-c')
const fixedSubtotal = toFinite(fixedRow?.subtotal)
if (fixedSubtotal != null) {
base.value = round3(fixedSubtotal)
return
}
const sum = rows.reduce((acc, row) => {
if (String(row?.id || '') === 'fixed-budget-c') return acc
const subtotal = toFinite(row?.subtotal)
return subtotal == null ? acc : acc + subtotal
}, 0)
base.value = round3(sum)
await zxFwPricingStore.loadContract(contractId)
const nextBase = zxFwPricingStore.getBaseSubtotal(contractId)
base.value = nextBase == null ? null : round3(nextBase)
} catch (error) {
console.error('load rate base failed:', error)
base.value = null
@ -127,12 +105,9 @@ watch(
)
watch(
() => pricingPaneReloadStore.persistedSeq,
() => contractVersion.value,
(nextVersion, prevVersion) => {
if (nextVersion === prevVersion || nextVersion === 0) return
const contractId = String(props.contractId || '').trim()
if (!contractId) return
if (!matchPricingPaneReload(pricingPaneReloadStore.lastPersistedEvent, contractId, ZXFW_RELOAD_SERVICE_KEY)) return
void loadBase()
}
)

View File

@ -7,7 +7,7 @@ import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorI
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { parseNumberOrNull } from '@/lib/number'
@ -1026,9 +1026,7 @@ const saveToIndexedDB = async () => {
field: 'investScale',
value: totalBudgetFee.value
})
if (synced) {
pricingPaneReloadStore.emit(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
}
if (!synced) return
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}

View File

@ -7,7 +7,7 @@ import { getMajorDictEntries, getServiceDictItemById, industryTypeList, isMajorI
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { addNumbers, decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
import { parseNumberOrNull } from '@/lib/number'
@ -879,9 +879,7 @@ const saveToIndexedDB = async () => {
field: 'landScale',
value: totalBudgetFee.value
})
if (synced) {
pricingPaneReloadStore.emit(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
}
if (!synced) return
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}

View File

@ -8,7 +8,7 @@ import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { formatThousands, formatThousandsFlexible } from '@/lib/numberFormat'
import { parseNumberOrNull } from '@/lib/number'
import { syncPricingTotalToZxFw, ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { syncPricingTotalToZxFw } from '@/lib/zxFwPricingSync'
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { loadConsultCategoryFactorMap } from '@/lib/xmFactorDefaults'
import MethodUnavailableNotice from '@/components/common/MethodUnavailableNotice.vue'
@ -432,9 +432,7 @@ const saveToIndexedDB = async () => {
field: 'workload',
value: totalServiceFee.value
})
if (synced) {
pricingPaneReloadStore.emit(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
}
if (!synced) return
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}

View File

@ -16,8 +16,7 @@ import {
getPricingMethodTotalsForServices,
type PricingMethodTotals
} from '@/lib/pricingMethodTotals'
import { ZXFW_RELOAD_SERVICE_KEY } from '@/lib/zxFwPricingSync'
import { matchPricingPaneReload, usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { usePricingPaneReloadStore } from '@/pinia/pricingPaneReload'
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next'
import {
AlertDialogAction,
@ -34,6 +33,7 @@ import { Button } from '@/components/ui/button'
import { TooltipProvider } from '@/components/ui/tooltip'
import { getServiceDictEntries, isIndustryEnabledByType, getIndustryTypeValue } from '@/sql'
import { useTabStore } from '@/pinia/tab'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import ServiceCheckboxSelector from '@/components/views/ServiceCheckboxSelector.vue'
interface ServiceItem {
@ -78,14 +78,16 @@ const props = defineProps<{
}>()
const tabStore = useTabStore()
const pricingPaneReloadStore = usePricingPaneReloadStore()
const DB_KEY = computed(() => `zxFW-${props.contractId}`)
const zxFwPricingStore = useZxFwPricingStore()
const PROJECT_INFO_KEY = 'xm-base-info-v1'
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
const PRICING_CLEAR_SKIP_TTL_MS = 5000
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
const projectIndustry = ref('')
const reloadSignal = ref(0)
const syncingFromStore = ref(false)
const localSavedVersion = ref(0)
const zxFwStoreVersion = computed(() => zxFwPricingStore.contractVersions[props.contractId] || 0)
type ServiceListItem = {
code?: string
@ -959,15 +961,18 @@ const saveToIndexedDB = async () => {
selectedIds: [...selectedIds.value],
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows()))
}
await localforage.setItem(DB_KEY.value, payload)
pricingPaneReloadStore.emitPersisted(props.contractId, ZXFW_RELOAD_SERVICE_KEY)
await zxFwPricingStore.setContractState(props.contractId, payload)
localSavedVersion.value = zxFwPricingStore.contractVersions[props.contractId] || 0
} catch (error) {
console.error('saveToIndexedDB failed:', error)
}
}
const loadFromIndexedDB = async () => {
if (syncingFromStore.value) return
syncingFromStore.value = true
try {
const data = await localforage.getItem<ZxFwState>(DB_KEY.value)
await zxFwPricingStore.loadContract(props.contractId)
const data = zxFwPricingStore.getContractState(props.contractId)
if (!data) {
selectedIds.value = []
detailRows.value = []
@ -1002,6 +1007,8 @@ const loadFromIndexedDB = async () => {
console.error('loadFromIndexedDB failed:', error)
selectedIds.value = []
detailRows.value = []
} finally {
syncingFromStore.value = false
}
}
@ -1017,22 +1024,11 @@ const loadProjectIndustry = async () => {
}
watch(
() => pricingPaneReloadStore.seq,
() => zxFwStoreVersion.value,
(nextVersion, prevVersion) => {
if (nextVersion === prevVersion || nextVersion === 0) return
if (!matchPricingPaneReload(pricingPaneReloadStore.lastEvent, props.contractId, ZXFW_RELOAD_SERVICE_KEY)) return
reloadSignal.value += 1
}
)
watch(
() => reloadSignal.value,
(nextVersion, prevVersion) => {
if (nextVersion === prevVersion || nextVersion === 0) return
void (async () => {
await loadFromIndexedDB()
await saveToIndexedDB()
})()
if (nextVersion === localSavedVersion.value) return
void loadFromIndexedDB()
}
)

View File

@ -1,21 +1,6 @@
import localforage from 'localforage'
import { toFiniteNumberOrNull } from '@/lib/number'
import { useZxFwPricingStore, type ZxFwPricingField } from '@/pinia/zxFwPricing'
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
interface ZxFwDetailRow {
id: string
investScale: number | null
landScale: number | null
workload: number | null
hourly: number | null
}
interface ZxFwState {
selectedIds?: string[]
selectedCodes?: string[]
detailRows: ZxFwDetailRow[]
}
export type { ZxFwPricingField } from '@/pinia/zxFwPricing'
export const ZXFW_RELOAD_SERVICE_KEY = 'zxfw-main'
@ -25,30 +10,6 @@ export const syncPricingTotalToZxFw = async (params: {
field: ZxFwPricingField
value: number | null | undefined
}) => {
const dbKey = `zxFW-${params.contractId}`
const data = await localforage.getItem<ZxFwState>(dbKey)
if (!data?.detailRows?.length) return false
const targetServiceId = String(params.serviceId)
const nextValue = toFiniteNumberOrNull(params.value)
let changed = false
const nextRows = data.detailRows.map(row => {
if (String(row.id) !== targetServiceId) return row
const currentValue = toFiniteNumberOrNull(row[params.field])
if (currentValue === nextValue) return row
changed = true
return {
...row,
[params.field]: nextValue
}
})
if (!changed) return false
await localforage.setItem(dbKey, {
...data,
detailRows: nextRows
})
return true
const store = useZxFwPricingStore()
return store.updatePricingField(params)
}

235
src/pinia/zxFwPricing.ts Normal file
View File

@ -0,0 +1,235 @@
import localforage from 'localforage'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { addNumbers } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number'
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
export interface ZxFwDetailRow {
id: string
code?: string
name?: string
investScale: number | null
landScale: number | null
workload: number | null
hourly: number | null
subtotal?: number | null
actions?: unknown
}
export interface ZxFwState {
selectedIds?: string[]
selectedCodes?: string[]
detailRows: ZxFwDetailRow[]
}
const FIXED_ROW_ID = 'fixed-budget-c'
const toKey = (contractId: string | number) => String(contractId || '').trim()
const dbKeyOf = (contractId: string) => `zxFW-${contractId}`
const round3 = (value: number) => Number(value.toFixed(3))
const toNumberOrZero = (value: unknown) => {
const numeric = Number(value)
return Number.isFinite(numeric) ? numeric : 0
}
const normalizeRows = (rows: unknown): ZxFwDetailRow[] =>
(Array.isArray(rows) ? rows : []).map(item => {
const row = item as Partial<ZxFwDetailRow>
return {
id: String(row.id || ''),
code: typeof row.code === 'string' ? row.code : '',
name: typeof row.name === 'string' ? row.name : '',
investScale: toFiniteNumberOrNull(row.investScale),
landScale: toFiniteNumberOrNull(row.landScale),
workload: toFiniteNumberOrNull(row.workload),
hourly: toFiniteNumberOrNull(row.hourly),
subtotal: toFiniteNumberOrNull(row.subtotal),
actions: row.actions
}
}).filter(row => row.id)
const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
const normalized = rows.map(row => ({ ...row }))
const nonFixedRows = normalized.filter(row => row.id !== FIXED_ROW_ID)
const totalInvestScale = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.investScale)), 0)
const totalLandScale = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.landScale)), 0)
const totalWorkload = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.workload)), 0)
const totalHourly = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.hourly)), 0)
const fixedSubtotal = addNumbers(totalInvestScale, totalLandScale, totalWorkload, totalHourly)
return normalized.map(row => {
if (row.id === FIXED_ROW_ID) {
return {
...row,
investScale: round3(totalInvestScale),
landScale: round3(totalLandScale),
workload: round3(totalWorkload),
hourly: round3(totalHourly),
subtotal: round3(fixedSubtotal)
}
}
const subtotal = addNumbers(
toNumberOrZero(row.investScale),
toNumberOrZero(row.landScale),
toNumberOrZero(row.workload),
toNumberOrZero(row.hourly)
)
return {
...row,
subtotal: round3(subtotal)
}
})
}
const normalizeState = (state: ZxFwState | null | undefined): ZxFwState => ({
selectedIds: Array.isArray(state?.selectedIds)
? state.selectedIds.map(id => String(id || '')).filter(Boolean)
: [],
selectedCodes: Array.isArray(state?.selectedCodes)
? state.selectedCodes.map(code => String(code || '')).filter(Boolean)
: [],
detailRows: applyRowSubtotals(normalizeRows(state?.detailRows))
})
const cloneState = (state: ZxFwState): ZxFwState => ({
selectedIds: [...(state.selectedIds || [])],
selectedCodes: [...(state.selectedCodes || [])],
detailRows: state.detailRows.map(row => ({ ...row }))
})
const loadTasks = new Map<string, Promise<ZxFwState>>()
const persistQueues = new Map<string, Promise<void>>()
export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
const contracts = ref<Record<string, ZxFwState>>({})
const contractVersions = ref<Record<string, number>>({})
const touchVersion = (contractId: string) => {
contractVersions.value[contractId] = (contractVersions.value[contractId] || 0) + 1
}
const queuePersist = async (contractId: string) => {
const prev = persistQueues.get(contractId) || Promise.resolve()
const next = prev
.then(async () => {
const current = contracts.value[contractId]
if (!current) return
await localforage.setItem<ZxFwState>(dbKeyOf(contractId), cloneState(current))
})
.catch(error => {
console.error('zxFwPricing persist failed:', error)
})
persistQueues.set(contractId, next)
await next
}
const getContractState = (contractIdRaw: string | number) => {
const contractId = toKey(contractIdRaw)
if (!contractId) return null
const data = contracts.value[contractId]
return data ? cloneState(data) : null
}
const loadContract = async (contractIdRaw: string | number, force = false) => {
const contractId = toKey(contractIdRaw)
if (!contractId) return null
if (!force && contracts.value[contractId]) return getContractState(contractId)
if (!force && loadTasks.has(contractId)) return loadTasks.get(contractId) as Promise<ZxFwState>
const task = (async () => {
const raw = await localforage.getItem<ZxFwState>(dbKeyOf(contractId))
const normalized = normalizeState(raw)
contracts.value[contractId] = normalized
touchVersion(contractId)
return cloneState(normalized)
})()
loadTasks.set(contractId, task)
try {
return await task
} finally {
loadTasks.delete(contractId)
}
}
const setContractState = async (contractIdRaw: string | number, state: ZxFwState) => {
const contractId = toKey(contractIdRaw)
if (!contractId) return
contracts.value[contractId] = normalizeState(state)
touchVersion(contractId)
await queuePersist(contractId)
}
const updatePricingField = async (params: {
contractId: string
serviceId: string | number
field: ZxFwPricingField
value: number | null | undefined
}) => {
const contractId = toKey(params.contractId)
if (!contractId) return false
if (!contracts.value[contractId]) {
await loadContract(contractId)
}
const current = contracts.value[contractId]
if (!current?.detailRows?.length) return false
const targetServiceId = String(params.serviceId || '').trim()
if (!targetServiceId) return false
const nextValue = toFiniteNumberOrNull(params.value)
let changed = false
const nextRows = current.detailRows.map(row => {
if (String(row.id || '') !== targetServiceId) return row
const oldValue = toFiniteNumberOrNull(row[params.field])
if (oldValue === nextValue) return row
changed = true
return {
...row,
[params.field]: nextValue
}
})
if (!changed) return false
contracts.value[contractId] = normalizeState({
...current,
detailRows: nextRows
})
touchVersion(contractId)
await queuePersist(contractId)
return true
}
const getBaseSubtotal = (contractIdRaw: string | number): number | null => {
const contractId = toKey(contractIdRaw)
if (!contractId) return null
const state = contracts.value[contractId]
if (!state?.detailRows?.length) return null
const fixedRow = state.detailRows.find(row => String(row.id || '') === FIXED_ROW_ID)
const fixedSubtotal = toFiniteNumberOrNull(fixedRow?.subtotal)
if (fixedSubtotal != null) return round3(fixedSubtotal)
const sum = state.detailRows.reduce((acc, row) => {
if (String(row.id || '') === FIXED_ROW_ID) return acc
const subtotal = toFiniteNumberOrNull(row.subtotal)
return subtotal == null ? acc : acc + subtotal
}, 0)
return round3(sum)
}
return {
contracts,
contractVersions,
getContractState,
loadContract,
setContractState,
updatePricingField,
getBaseSubtotal
}
})