784 lines
26 KiB
TypeScript
784 lines
26 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 totalAmount = sumByNumber(rowsFromDb || [], row =>
|
|
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
|
|
)
|
|
const onlyRow = (rowsFromDb || []).find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID)
|
|
const consultCategoryFactor =
|
|
toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ??
|
|
consultCategoryFactorMap?.get(String(serviceId)) ??
|
|
getDefaultConsultCategoryFactor(serviceId)
|
|
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
|
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 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 onlyRow = (rowsFromDb || []).find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID)
|
|
const consultCategoryFactor =
|
|
toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ??
|
|
consultCategoryFactorMap?.get(String(serviceId)) ??
|
|
getDefaultConsultCategoryFactor(serviceId)
|
|
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
|
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: ONLY_COST_SCALE_ROW_ID,
|
|
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
|
|
}
|