JGJS2026/src/lib/pricingMethodTotals.ts
2026-03-09 15:44:56 +08:00

813 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import localforage from 'localforage'
import {
expertList,
getMajorDictEntries,
getMajorIdAliasMap,
getServiceDictById,
taskList
} from '@/sql'
import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { toFiniteNumberOrNull } from '@/lib/number'
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
interface StoredDetailRowsState<T = any> {
detailRows?: T[]
}
interface StoredFactorState {
detailRows?: Array<{
id: string
standardFactor?: number | null
budgetValue?: number | null
}>
}
type MaybeNumber = number | null | undefined
interface ScaleRow {
id: string
amount: number | null
landArea: number | null
consultCategoryFactor: number | null
majorFactor: number | null
workStageFactor: number | null
workRatio: number | null
}
interface WorkloadRow {
id: string
conversion: number | null
workload: number | null
basicFee: number | null
budgetAdoptedUnitPrice: number | null
consultCategoryFactor: number | null
}
interface HourlyRow {
id: string
adoptedBudgetUnitPrice: number | null
personnelCount: number | null
workdayCount: number | null
}
interface MajorLite {
code: string
defCoe: number | null
hasCost?: boolean
hasArea?: boolean
industryId?: string | number | null
}
interface ServiceLite {
defCoe: number | null
onlyCostScale?: boolean | null
}
interface TaskLite {
serviceID: number
conversion: number | null
defPrice: number | null
}
interface ExpertLite {
defPrice: number | null
manageCoe: number | null
}
interface XmBaseInfoState {
projectIndustry?: string
}
export interface PricingMethodTotals {
investScale: number | null
landScale: number | null
workload: number | null
hourly: number | null
}
interface PricingMethodTotalsOptions {
excludeInvestmentCostAndAreaRows?: boolean
}
interface PricingMethodDetailDbKeys {
investScale: string
landScale: string
workload: string
hourly: string
}
interface PricingMethodDefaultDetailRows {
investScale: unknown[]
landScale: unknown[]
workload: unknown[]
hourly: unknown[]
}
interface PricingMethodDefaultBuildContext {
htData: StoredDetailRowsState | null
consultCategoryFactorMap: Map<string, number | null>
majorFactorMap: Map<string, number | null>
industryId: string
excludeInvestmentCostAndAreaRows: boolean
}
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
const hasOwn = (obj: unknown, key: string) =>
Object.prototype.hasOwnProperty.call(obj || {}, key)
const toRowMap = <TRow extends { id: string }>(rows?: TRow[]) => {
const map = new Map<string, TRow>()
for (const row of rows || []) {
map.set(String(row.id), row)
}
return map
}
const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return toFiniteNumberOrNull(service?.defCoe)
}
const isOnlyCostScaleService = (serviceId: string | number) => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return service?.onlyCostScale === true
}
const majorById = new Map(getMajorDictEntries().map(({ id, item }) => [id, item as MajorLite]))
const majorIdAliasMap = getMajorIdAliasMap()
const getDefaultMajorFactorById = (id: string) => {
const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id
const major = majorById.get(resolvedId)
return toFiniteNumberOrNull(major?.defCoe)
}
const isCostMajorById = (id: string) => {
const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id
const major = majorById.get(resolvedId)
if (!major) return false
return major.hasCost !== false
}
const isAreaMajorById = (id: string) => {
const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id
const major = majorById.get(resolvedId)
if (!major) return false
return major.hasArea !== false
}
const isDualScaleMajorById = (id: string) => {
const resolvedId = majorById.has(id) ? id : majorIdAliasMap.get(id) || id
const major = majorById.get(resolvedId)
if (!major) return false
const hasCost = major.hasCost !== false
const hasArea = major.hasArea !== false
return hasCost && hasArea
}
const getIndustryMajorEntryByIndustryId = (industryId: string | null | undefined) => {
const key = String(industryId || '').trim()
if (!key) return null
for (const [id, item] of majorById.entries()) {
const majorIndustryId = String(item?.industryId ?? '').trim()
if (majorIndustryId === key && !String(item?.code || '').includes('-')) {
return { id, item }
}
}
return null
}
const resolveFactorValue = (
row: { budgetValue?: number | null; standardFactor?: number | null } | undefined,
fallback: number | null
) => {
if (!row) return fallback
const budgetValue = toFiniteNumberOrNull(row.budgetValue)
if (budgetValue != null) return budgetValue
const standardFactor = toFiniteNumberOrNull(row.standardFactor)
if (standardFactor != null) return standardFactor
return fallback
}
const buildConsultCategoryFactorMap = (state: StoredFactorState | null) => {
const map = new Map<string, number | null>()
const serviceDict = getServiceDictById() as Record<string, ServiceLite | undefined>
for (const [id, item] of Object.entries(serviceDict)) {
map.set(String(id), toFiniteNumberOrNull(item?.defCoe))
}
for (const row of state?.detailRows || []) {
if (!row?.id) continue
const id = String(row.id)
map.set(id, resolveFactorValue(row, map.get(id) ?? null))
}
return map
}
const buildMajorFactorMap = (state: StoredFactorState | null) => {
const map = new Map<string, number | null>()
for (const [id, item] of majorById.entries()) {
map.set(String(id), toFiniteNumberOrNull(item?.defCoe))
}
for (const row of state?.detailRows || []) {
if (!row?.id) continue
const rowId = String(row.id)
const id = map.has(rowId) ? rowId : majorIdAliasMap.get(rowId) || rowId
map.set(id, resolveFactorValue(row, map.get(id) ?? null))
}
return map
}
const getMajorLeafIds = () =>
getMajorDictEntries()
.filter(({ item }) => Boolean(item?.code && String(item.code).includes('-')))
.map(({ id }) => id)
const buildDefaultScaleRows = (
serviceId: string | number,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>
): ScaleRow[] => {
const defaultConsultCategoryFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return getMajorLeafIds().map(id => ({
id,
amount: null,
landArea: null,
consultCategoryFactor: defaultConsultCategoryFactor,
majorFactor: majorFactorMap?.get(id) ?? getDefaultMajorFactorById(id),
workStageFactor: 1,
workRatio: 100
}))
}
const mergeScaleRows = (
serviceId: string | number,
rowsFromDb: Array<Partial<ScaleRow> & Pick<ScaleRow, 'id'>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>
): ScaleRow[] => {
const dbValueMap = toRowMap(rowsFromDb)
for (const row of rowsFromDb || []) {
const rowId = String(row.id)
const nextId = majorIdAliasMap.get(rowId)
if (nextId && !dbValueMap.has(nextId)) {
dbValueMap.set(nextId, row as ScaleRow)
}
}
const defaultConsultCategoryFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return buildDefaultScaleRows(serviceId, consultCategoryFactorMap, majorFactorMap).map(row => {
const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row
const hasConsultCategoryFactor = hasOwn(fromDb, 'consultCategoryFactor')
const hasMajorFactor = hasOwn(fromDb, 'majorFactor')
const hasWorkStageFactor = hasOwn(fromDb, 'workStageFactor')
const hasWorkRatio = hasOwn(fromDb, 'workRatio')
return {
...row,
amount: toFiniteNumberOrNull(fromDb.amount),
landArea: toFiniteNumberOrNull(fromDb.landArea),
consultCategoryFactor:
toFiniteNumberOrNull(fromDb.consultCategoryFactor) ??
(hasConsultCategoryFactor ? null : defaultConsultCategoryFactor),
majorFactor:
toFiniteNumberOrNull(fromDb.majorFactor) ??
(hasMajorFactor ? null : (majorFactorMap?.get(row.id) ?? getDefaultMajorFactorById(row.id))),
workStageFactor:
toFiniteNumberOrNull((fromDb as Partial<ScaleRow>).workStageFactor) ??
(hasWorkStageFactor ? null : row.workStageFactor),
workRatio:
toFiniteNumberOrNull((fromDb as Partial<ScaleRow>).workRatio) ??
(hasWorkRatio ? null : row.workRatio)
}
})
}
const getBenchmarkBudgetByAmount = (amount: MaybeNumber) =>
getBenchmarkBudgetByScale(amount, 'cost')
const getBenchmarkBudgetByLandArea = (landArea: MaybeNumber) =>
getBenchmarkBudgetByScale(landArea, 'area')
const getInvestmentBudgetFee = (row: ScaleRow) => {
return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByAmount(row.amount),
majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor,
workStageFactor: row.workStageFactor,
workRatio: row.workRatio
})
}
const getOnlyCostScaleBudgetFee = (
serviceId: string,
rowsFromDb: Array<Record<string, unknown>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>,
industryId?: string | null
) => {
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
const sourceRows = rowsFromDb || []
const defaultConsultCategoryFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
const defaultMajorFactor =
(industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ??
toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ??
1
// 新版 onlyCostScale 支持“按项目行”存储(如 1::majorId、2::majorId每行需独立计费后求和。
const usePerRowCalculation = sourceRows.some(row => {
if (typeof row?.projectIndex === 'number' && Number.isFinite(row.projectIndex)) return true
const id = String(row?.id || '')
return /^\d+::/.test(id)
})
if (usePerRowCalculation) {
return sumByNumber(sourceRows, row => {
const amount = toFiniteNumberOrNull(row?.amount)
if (amount == null) return null
return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByAmount(amount),
majorFactor: toFiniteNumberOrNull(row?.majorFactor) ?? defaultMajorFactor,
consultCategoryFactor: toFiniteNumberOrNull(row?.consultCategoryFactor) ?? defaultConsultCategoryFactor,
workStageFactor: toFiniteNumberOrNull(row?.workStageFactor) ?? 1,
workRatio: toFiniteNumberOrNull(row?.workRatio) ?? 100
})
})
}
const totalAmount = sumByNumber(sourceRows, row =>
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
)
const onlyRow =
sourceRows.find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
sourceRows.find(row => hasOwn(row, 'consultCategoryFactor') || hasOwn(row, 'majorFactor')) ||
sourceRows[0]
const consultCategoryFactor =
toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ?? defaultConsultCategoryFactor
const majorFactor =
toFiniteNumberOrNull(onlyRow?.majorFactor) ?? defaultMajorFactor
const workStageFactor = toFiniteNumberOrNull(onlyRow?.workStageFactor) ?? 1
const workRatio = toFiniteNumberOrNull(onlyRow?.workRatio) ?? 100
return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByAmount(totalAmount),
majorFactor,
consultCategoryFactor,
workStageFactor,
workRatio
})
}
const buildOnlyCostScaleDetailRows = (
serviceId: string,
rowsFromDb: Array<Record<string, unknown>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>,
industryId?: string | null
) => {
const totalAmount = sumByNumber(rowsFromDb || [], row =>
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
)
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
const onlyCostRowId = industryMajorEntry?.id || ONLY_COST_SCALE_ROW_ID
const onlyRow =
(rowsFromDb || []).find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
(rowsFromDb || []).find(row => String(row?.id || '') === onlyCostRowId)
const consultCategoryFactor =
toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ??
consultCategoryFactorMap?.get(String(serviceId)) ??
getDefaultConsultCategoryFactor(serviceId)
const majorFactor =
toFiniteNumberOrNull(onlyRow?.majorFactor) ??
(industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ??
toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ??
1
const workStageFactor = toFiniteNumberOrNull(onlyRow?.workStageFactor) ?? 1
const workRatio = toFiniteNumberOrNull(onlyRow?.workRatio) ?? 100
return [
{
id: onlyCostRowId,
amount: totalAmount,
consultCategoryFactor,
majorFactor,
workStageFactor,
workRatio,
benchmarkBudgetBasicChecked: typeof onlyRow?.benchmarkBudgetBasicChecked === 'boolean' ? onlyRow.benchmarkBudgetBasicChecked : true,
benchmarkBudgetOptionalChecked: typeof onlyRow?.benchmarkBudgetOptionalChecked === 'boolean' ? onlyRow.benchmarkBudgetOptionalChecked : true
}
]
}
const getLandBudgetFee = (row: ScaleRow) => {
return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea),
majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor,
workStageFactor: row.workStageFactor,
workRatio: row.workRatio
})
}
const getTaskEntriesByServiceId = (serviceId: string | number) =>
Object.entries(taskList as Record<string, TaskLite>)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.filter(([, task]) => Number(task.serviceID) === Number(serviceId))
const buildDefaultWorkloadRows = (
serviceId: string | number,
consultCategoryFactorMap?: Map<string, number | null>
): WorkloadRow[] => {
const defaultConsultCategoryFactor =
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return getTaskEntriesByServiceId(serviceId).map(([taskId, task], order) => ({
id: `task-${taskId}-${order}`,
conversion: toFiniteNumberOrNull(task.conversion),
workload: null,
basicFee: null,
budgetAdoptedUnitPrice: toFiniteNumberOrNull(task.defPrice),
consultCategoryFactor: defaultConsultCategoryFactor
}))
}
const mergeWorkloadRows = (
serviceId: string | number,
rowsFromDb: Array<Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>
): WorkloadRow[] => {
const dbValueMap = toRowMap(rowsFromDb)
return buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap).map(row => {
const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row
return {
...row,
workload: toFiniteNumberOrNull(fromDb.workload),
basicFee: toFiniteNumberOrNull(fromDb.basicFee),
budgetAdoptedUnitPrice: toFiniteNumberOrNull(fromDb.budgetAdoptedUnitPrice),
consultCategoryFactor: toFiniteNumberOrNull(fromDb.consultCategoryFactor)
}
})
}
const calcWorkloadBasicFee = (row: WorkloadRow) => {
if (
row.budgetAdoptedUnitPrice == null ||
row.conversion == null ||
row.workload == null
) {
return null
}
return roundTo(
toDecimal(row.budgetAdoptedUnitPrice).mul(row.conversion).mul(row.workload),
2
)
}
const calcWorkloadServiceFee = (row: WorkloadRow) => {
if (row.consultCategoryFactor == null) {
return null
}
const basicFee = row.basicFee ?? calcWorkloadBasicFee(row)
if (basicFee == null) return null
return roundTo(
toDecimal(basicFee).mul(row.consultCategoryFactor),
2
)
}
const getExpertEntries = () =>
Object.entries(expertList as Record<string, ExpertLite>).sort((a, b) => Number(a[0]) - Number(b[0]))
const getDefaultHourlyAdoptedPrice = (expert: ExpertLite) => {
if (expert.defPrice == null || expert.manageCoe == null) return null
return roundTo(toDecimal(expert.defPrice).mul(expert.manageCoe), 2)
}
const buildDefaultHourlyRows = (): HourlyRow[] =>
getExpertEntries().map(([expertId, expert]) => ({
id: `expert-${expertId}`,
adoptedBudgetUnitPrice: getDefaultHourlyAdoptedPrice(expert),
personnelCount: null,
workdayCount: null
}))
const mergeHourlyRows = (
rowsFromDb: Array<Partial<HourlyRow> & Pick<HourlyRow, 'id'>> | undefined
): HourlyRow[] => {
const dbValueMap = toRowMap(rowsFromDb)
return buildDefaultHourlyRows().map(row => {
const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row
return {
...row,
adoptedBudgetUnitPrice: toFiniteNumberOrNull(fromDb.adoptedBudgetUnitPrice),
personnelCount: toFiniteNumberOrNull(fromDb.personnelCount),
workdayCount: toFiniteNumberOrNull(fromDb.workdayCount)
}
})
}
const calcHourlyServiceBudget = (row: HourlyRow) => {
if (row.adoptedBudgetUnitPrice == null || row.personnelCount == null || row.workdayCount == null) return null
return roundTo(toDecimal(row.adoptedBudgetUnitPrice).mul(row.personnelCount).mul(row.workdayCount), 2)
}
const resolveScaleRows = (
serviceId: string,
pricingData: StoredDetailRowsState | null,
htData: StoredDetailRowsState | null,
consultCategoryFactorMap?: Map<string, number | null>,
majorFactorMap?: Map<string, number | null>
) => {
if (pricingData?.detailRows != null) {
return mergeScaleRows(
serviceId,
pricingData.detailRows as any,
consultCategoryFactorMap,
majorFactorMap
)
}
if (htData?.detailRows != null) {
return mergeScaleRows(
serviceId,
htData.detailRows as any,
consultCategoryFactorMap,
majorFactorMap
)
}
return buildDefaultScaleRows(serviceId, consultCategoryFactorMap, majorFactorMap)
}
export const getPricingMethodDetailDbKeys = (
contractId: string,
serviceId: string | number
): PricingMethodDetailDbKeys => {
const normalizedServiceId = String(serviceId)
return {
investScale: `tzGMF-${contractId}-${normalizedServiceId}`,
landScale: `ydGMF-${contractId}-${normalizedServiceId}`,
workload: `gzlF-${contractId}-${normalizedServiceId}`,
hourly: `hourlyPricing-${contractId}-${normalizedServiceId}`
}
}
const loadPricingMethodDefaultBuildContext = async (
contractId: string,
options?: PricingMethodTotalsOptions
): Promise<PricingMethodDefaultBuildContext> => {
const htDbKey = `ht-info-v3-${contractId}`
const consultFactorDbKey = `ht-consult-category-factor-v1-${contractId}`
const majorFactorDbKey = `ht-major-factor-v1-${contractId}`
const baseInfoDbKey = 'xm-base-info-v1'
const [htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([
localforage.getItem<StoredDetailRowsState>(htDbKey),
localforage.getItem<StoredFactorState>(consultFactorDbKey),
localforage.getItem<StoredFactorState>(majorFactorDbKey),
localforage.getItem<XmBaseInfoState>(baseInfoDbKey)
])
return {
htData,
consultCategoryFactorMap: buildConsultCategoryFactorMap(consultFactorData),
majorFactorMap: buildMajorFactorMap(majorFactorData),
industryId: typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : '',
excludeInvestmentCostAndAreaRows: options?.excludeInvestmentCostAndAreaRows === true
}
}
const buildDefaultPricingMethodDetailRows = (
serviceId: string,
context: PricingMethodDefaultBuildContext
): PricingMethodDefaultDetailRows => {
const onlyCostScale = isOnlyCostScaleService(serviceId)
const scaleRows = resolveScaleRows(
serviceId,
null,
context.htData,
context.consultCategoryFactorMap,
context.majorFactorMap
)
const investScale = onlyCostScale
? buildOnlyCostScaleDetailRows(
serviceId,
context.htData?.detailRows as Array<Record<string, unknown>> | undefined,
context.consultCategoryFactorMap,
context.majorFactorMap,
context.industryId
)
: scaleRows.filter(row => {
if (!isCostMajorById(row.id)) return false
if (context.excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return false
return true
})
const landScale = scaleRows.filter(row => isAreaMajorById(row.id))
return {
investScale,
landScale,
workload: buildDefaultWorkloadRows(serviceId, context.consultCategoryFactorMap),
hourly: buildDefaultHourlyRows()
}
}
export const persistDefaultPricingMethodDetailRowsForServices = async (params: {
contractId: string
serviceIds: Array<string | number>
options?: PricingMethodTotalsOptions
}) => {
const uniqueServiceIds = Array.from(new Set(params.serviceIds.map(serviceId => String(serviceId))))
if (uniqueServiceIds.length === 0) return
const context = await loadPricingMethodDefaultBuildContext(params.contractId, params.options)
await Promise.all(
uniqueServiceIds.map(async serviceId => {
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
const defaultRows = buildDefaultPricingMethodDetailRows(serviceId, context)
console.log(dbKeys,defaultRows)
await Promise.all([
localforage.setItem(dbKeys.investScale, { detailRows: defaultRows.investScale }),
localforage.setItem(dbKeys.landScale, { detailRows: defaultRows.landScale }),
localforage.setItem(dbKeys.workload, { detailRows: defaultRows.workload }),
localforage.setItem(dbKeys.hourly, { detailRows: defaultRows.hourly })
])
})
)
}
export const getPricingMethodTotalsForService = async (params: {
contractId: string
serviceId: string | number
options?: PricingMethodTotalsOptions
}): Promise<PricingMethodTotals> => {
const serviceId = String(params.serviceId)
const htDbKey = `ht-info-v3-${params.contractId}`
const consultFactorDbKey = `ht-consult-category-factor-v1-${params.contractId}`
const majorFactorDbKey = `ht-major-factor-v1-${params.contractId}`
const baseInfoDbKey = 'xm-base-info-v1'
const investDbKey = `tzGMF-${params.contractId}-${serviceId}`
const landDbKey = `ydGMF-${params.contractId}-${serviceId}`
const workloadDbKey = `gzlF-${params.contractId}-${serviceId}`
const hourlyDbKey = `hourlyPricing-${params.contractId}-${serviceId}`
const [investData, landData, workloadData, hourlyData, htData, consultFactorData, majorFactorData, baseInfo] = await Promise.all([
localforage.getItem<StoredDetailRowsState>(investDbKey),
localforage.getItem<StoredDetailRowsState>(landDbKey),
localforage.getItem<StoredDetailRowsState>(workloadDbKey),
localforage.getItem<StoredDetailRowsState>(hourlyDbKey),
localforage.getItem<StoredDetailRowsState>(htDbKey),
localforage.getItem<StoredFactorState>(consultFactorDbKey),
localforage.getItem<StoredFactorState>(majorFactorDbKey),
localforage.getItem<XmBaseInfoState>(baseInfoDbKey)
])
const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData)
const majorFactorMap = buildMajorFactorMap(majorFactorData)
const onlyCostScale = isOnlyCostScaleService(serviceId)
const industryId = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
// 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。
const excludeInvestmentCostAndAreaRows = params.options?.excludeInvestmentCostAndAreaRows === true
const investScale = onlyCostScale
? getOnlyCostScaleBudgetFee(
serviceId,
(investData?.detailRows as Array<Record<string, unknown>> | undefined) ||
(htData?.detailRows as Array<Record<string, unknown>> | undefined),
consultCategoryFactorMap,
majorFactorMap,
industryId
)
: (() => {
const investRows = resolveScaleRows(
serviceId,
investData,
htData,
consultCategoryFactorMap,
majorFactorMap
)
return sumByNumber(investRows, row => {
if (!isCostMajorById(row.id)) return null
if (excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return null
return getInvestmentBudgetFee(row)
})
})()
const landRows = resolveScaleRows(
serviceId,
landData,
htData,
consultCategoryFactorMap,
majorFactorMap
)
const landScale = sumByNumber(landRows, row => (isAreaMajorById(row.id) ? getLandBudgetFee(row) : null))
const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap)
const workload =
defaultWorkloadRows.length === 0
? null
: sumByNumber(
workloadData?.detailRows != null
? mergeWorkloadRows(serviceId, workloadData.detailRows as any, consultCategoryFactorMap)
: defaultWorkloadRows,
row => calcWorkloadServiceFee(row)
)
const hourlyRows =
hourlyData?.detailRows != null
? mergeHourlyRows(hourlyData.detailRows as any)
: buildDefaultHourlyRows()
const hourly = sumByNumber(hourlyRows, row => calcHourlyServiceBudget(row))
return {
investScale,
landScale,
workload,
hourly
}
}
export const ensurePricingMethodDetailRowsForServices = async (params: {
contractId: string
serviceIds: Array<string | number>
options?: PricingMethodTotalsOptions
}) => {
const uniqueServiceIds = Array.from(new Set(params.serviceIds.map(serviceId => String(serviceId))))
if (uniqueServiceIds.length === 0) return
const context = await loadPricingMethodDefaultBuildContext(params.contractId, params.options)
await Promise.all(
uniqueServiceIds.map(async serviceId => {
const dbKeys = getPricingMethodDetailDbKeys(params.contractId, serviceId)
const [investData, landData, workloadData, hourlyData] = await Promise.all([
localforage.getItem<StoredDetailRowsState>(dbKeys.investScale),
localforage.getItem<StoredDetailRowsState>(dbKeys.landScale),
localforage.getItem<StoredDetailRowsState>(dbKeys.workload),
localforage.getItem<StoredDetailRowsState>(dbKeys.hourly)
])
const shouldInitInvest = !Array.isArray(investData?.detailRows) || investData!.detailRows!.length === 0
const shouldInitLand = !Array.isArray(landData?.detailRows) || landData!.detailRows!.length === 0
const shouldInitWorkload = !Array.isArray(workloadData?.detailRows) || workloadData!.detailRows!.length === 0
const shouldInitHourly = !Array.isArray(hourlyData?.detailRows) || hourlyData!.detailRows!.length === 0
const writeTasks: Promise<unknown>[] = []
let defaultRows: PricingMethodDefaultDetailRows | null = null
const getDefaultRows = () => {
if (!defaultRows) {
defaultRows = buildDefaultPricingMethodDetailRows(serviceId, context)
}
return defaultRows
}
if (shouldInitInvest) {
writeTasks.push(localforage.setItem(dbKeys.investScale, { detailRows: getDefaultRows().investScale }))
}
if (shouldInitLand) {
writeTasks.push(localforage.setItem(dbKeys.landScale, { detailRows: getDefaultRows().landScale }))
}
if (shouldInitWorkload) {
writeTasks.push(localforage.setItem(dbKeys.workload, { detailRows: getDefaultRows().workload }))
}
if (shouldInitHourly) {
writeTasks.push(localforage.setItem(dbKeys.hourly, { detailRows: getDefaultRows().hourly }))
}
if (writeTasks.length > 0) {
await Promise.all(writeTasks)
}
})
)
}
export const getPricingMethodTotalsForServices = async (params: {
contractId: string
serviceIds: Array<string | number>
options?: PricingMethodTotalsOptions
}) => {
const result = new Map<string, PricingMethodTotals>()
await Promise.all(
params.serviceIds.map(async serviceId => {
const totals = await getPricingMethodTotalsForService({
contractId: params.contractId,
serviceId,
options: params.options
})
result.set(String(serviceId), totals)
})
)
return result
}