813 lines
28 KiB
TypeScript
813 lines
28 KiB
TypeScript
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
|
||
}
|