调整存储的逻辑
This commit is contained in:
parent
bbc8777b74
commit
3ad7bae1a9
@ -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)
|
||||
|
||||
@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -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
235
src/pinia/zxFwPricing.ts
Normal 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
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user