完成大部分

This commit is contained in:
zwgczx committed 2026-02-28 16:34:36 +08:00
1 parent e97707ac59
commit 13b03e016e
21 files changed
+1628 -275

No files matched your search

+317
View File
@@ -0,0 +1,317 @@
import localforage from 'localforage'
import { expertList, getBasicFeeFromScale, majorList, serviceList, taskList } from '@/sql'
import { addNumbers, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
interface StoredDetailRowsState<T = any> {
detailRows?: T[]
}
type MaybeNumber = number | null | undefined
interface ScaleRow {
id: string
amount: number | null
landArea: number | null
consultCategoryFactor: number | null
majorFactor: number | null
}
interface WorkloadRow {
id: string
conversion: number | null
workload: 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
}
interface ServiceLite {
defCoe: number | null
}
interface TaskLite {
serviceID: number
conversion: number | null
defPrice: number | null
}
interface ExpertLite {
defPrice: number | null
manageCoe: number | null
}
export interface PricingMethodTotals {
investScale: number | null
landScale: number | null
workload: number | null
hourly: number | null
}
const toFiniteNumberOrNull = (value: unknown): number | null =>
typeof value === 'number' && Number.isFinite(value) ? value : null
const hasOwn = (obj: unknown, key: string) =>
Object.prototype.hasOwnProperty.call(obj || {}, key)
const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
const service = (serviceList as Record<string, ServiceLite | undefined>)[String(serviceId)]
return toFiniteNumberOrNull(service?.defCoe)
}
const getDefaultMajorFactorById = (id: string) => {
const major = (majorList as Record<string, MajorLite | undefined>)[id]
return toFiniteNumberOrNull(major?.defCoe)
}
const getMajorLeafIds = () =>
Object.entries(majorList as Record<string, MajorLite>)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.filter(([, item]) => Boolean(item?.code && item.code.includes('-')))
.map(([id]) => id)
const buildDefaultScaleRows = (serviceId: string | number): ScaleRow[] => {
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
return getMajorLeafIds().map(id => ({
id,
amount: null,
landArea: null,
consultCategoryFactor: defaultConsultCategoryFactor,
majorFactor: getDefaultMajorFactorById(id)
}))
}
const mergeScaleRows = (
serviceId: string | number,
rowsFromDb: Array<Partial<ScaleRow> & Pick<ScaleRow, 'id'>> | undefined
): ScaleRow[] => {
const dbValueMap = new Map<string, Partial<ScaleRow> & Pick<ScaleRow, 'id'>>()
for (const row of rowsFromDb || []) {
dbValueMap.set(String(row.id), row)
}
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
return buildDefaultScaleRows(serviceId).map(row => {
const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row
const hasConsultCategoryFactor = hasOwn(fromDb, 'consultCategoryFactor')
const hasMajorFactor = hasOwn(fromDb, 'majorFactor')
return {
...row,
amount: toFiniteNumberOrNull(fromDb.amount),
landArea: toFiniteNumberOrNull(fromDb.landArea),
consultCategoryFactor:
toFiniteNumberOrNull(fromDb.consultCategoryFactor) ??
(hasConsultCategoryFactor ? null : defaultConsultCategoryFactor),
majorFactor:
toFiniteNumberOrNull(fromDb.majorFactor) ??
(hasMajorFactor ? null : getDefaultMajorFactorById(row.id))
}
})
}
const getBenchmarkBudgetByAmount = (amount: MaybeNumber) => {
const result = getBasicFeeFromScale(amount, 'cost')
return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
}
const getBenchmarkBudgetByLandArea = (landArea: MaybeNumber) => {
const result = getBasicFeeFromScale(landArea, 'area')
return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
}
const getInvestmentBudgetFee = (row: ScaleRow) => {
const benchmarkBudget = getBenchmarkBudgetByAmount(row.amount)
if (benchmarkBudget == null || row.majorFactor == null || row.consultCategoryFactor == null) return null
return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2)
}
const getLandBudgetFee = (row: ScaleRow) => {
const benchmarkBudget = getBenchmarkBudgetByLandArea(row.landArea)
if (benchmarkBudget == null || row.majorFactor == null || row.consultCategoryFactor == null) return null
return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2)
}
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): WorkloadRow[] => {
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
return getTaskEntriesByServiceId(serviceId).map(([taskId, task], order) => ({
id: `task-${taskId}-${order}`,
conversion: toFiniteNumberOrNull(task.conversion),
workload: null,
budgetAdoptedUnitPrice: toFiniteNumberOrNull(task.defPrice),
consultCategoryFactor: defaultConsultCategoryFactor
}))
}
const mergeWorkloadRows = (
serviceId: string | number,
rowsFromDb: Array<Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>> | undefined
): WorkloadRow[] => {
const dbValueMap = new Map<string, Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>>()
for (const row of rowsFromDb || []) {
dbValueMap.set(String(row.id), row)
}
return buildDefaultWorkloadRows(serviceId).map(row => {
const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row
return {
...row,
workload: toFiniteNumberOrNull(fromDb.workload),
budgetAdoptedUnitPrice: toFiniteNumberOrNull(fromDb.budgetAdoptedUnitPrice),
consultCategoryFactor: toFiniteNumberOrNull(fromDb.consultCategoryFactor)
}
})
}
const calcWorkloadServiceFee = (row: WorkloadRow) => {
if (
row.budgetAdoptedUnitPrice == null ||
row.conversion == null ||
row.workload == null ||
row.consultCategoryFactor == null
) {
return null
}
return roundTo(
toDecimal(row.budgetAdoptedUnitPrice).mul(row.conversion).mul(row.workload).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 = new Map<string, Partial<HourlyRow> & Pick<HourlyRow, 'id'>>()
for (const row of rowsFromDb || []) {
dbValueMap.set(String(row.id), row)
}
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)
}
export const getPricingMethodTotalsForService = async (params: {
contractId: string
serviceId: string | number
}): Promise<PricingMethodTotals> => {
const serviceId = String(params.serviceId)
const htDbKey = `ht-info-v3-${params.contractId}`
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] = await Promise.all([
localforage.getItem<StoredDetailRowsState>(investDbKey),
localforage.getItem<StoredDetailRowsState>(landDbKey),
localforage.getItem<StoredDetailRowsState>(workloadDbKey),
localforage.getItem<StoredDetailRowsState>(hourlyDbKey),
localforage.getItem<StoredDetailRowsState>(htDbKey)
])
const investRows =
investData?.detailRows != null
? mergeScaleRows(serviceId, investData.detailRows as any)
: htData?.detailRows != null
? mergeScaleRows(serviceId, htData.detailRows as any)
: buildDefaultScaleRows(serviceId)
const investScale = sumByNumber(investRows, row => getInvestmentBudgetFee(row))
const landRows =
landData?.detailRows != null
? mergeScaleRows(serviceId, landData.detailRows as any)
: htData?.detailRows != null
? mergeScaleRows(serviceId, htData.detailRows as any)
: buildDefaultScaleRows(serviceId)
const landScale = sumByNumber(landRows, row => getLandBudgetFee(row))
const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId)
const workload =
defaultWorkloadRows.length === 0
? null
: sumByNumber(
workloadData?.detailRows != null
? mergeWorkloadRows(serviceId, workloadData.detailRows as any)
: 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 getPricingMethodTotalsForServices = async (params: {
contractId: string
serviceIds: Array<string | number>
}) => {
const result = new Map<string, PricingMethodTotals>()
await Promise.all(
params.serviceIds.map(async serviceId => {
const totals = await getPricingMethodTotalsForService({
contractId: params.contractId,
serviceId
})
result.set(String(serviceId), totals)
})
)
return result
}