完成大部分
This commit is contained in:
1 parent
e97707ac59
commit
13b03e016e
21 files changed
+1628
-275
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user