calculator2026/src/lib/reportExportBuilders.ts
2026-03-25 10:40:19 +08:00

627 lines
19 KiB
TypeScript

import { serviceList } from '@/sql'
import { roundTo, toFiniteNumber, toFiniteNumberOrZero } from '@/lib/decimal'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
export { toFiniteNumber, toFiniteNumberOrZero }
interface ScaleMethodRowLike {
id: string
majorDictId?: unknown
amount: number | null
landArea: number | null
benchmarkBudgetBasicChecked?: unknown
benchmarkBudgetOptionalChecked?: unknown
basicFormula?: unknown
optionalFormula?: unknown
budgetFee?: unknown
budgetFeeBasic?: unknown
budgetFeeOptional?: unknown
consultCategoryFactor?: unknown
majorFactor?: unknown
workStageFactor?: unknown
workRatio?: unknown
remark?: unknown
}
interface WorkContentRowLike {
id?: unknown
content?: unknown
checked?: unknown
custom?: unknown
serviceid?: unknown
isAddTrigger?: unknown
}
interface FactorRowLike {
id: string
budgetValue?: unknown
remark?: unknown
}
interface ScaleRowLike {
id: string
amount: number | null
landArea: number | null
}
interface WorkloadMethodRowLike {
id: string
budgetAdoptedUnitPrice?: unknown
workload?: unknown
basicFee?: unknown
consultCategoryFactor?: unknown
serviceFee?: unknown
remark?: unknown
}
interface HourlyMethodRowLike {
id: string
adoptedBudgetUnitPrice?: unknown
personnelCount?: unknown
workdayCount?: unknown
serviceBudget?: unknown
remark?: unknown
}
interface ZxFwRowLike {
id: string
process?: unknown
subtotal?: unknown
finalFee?: unknown
investScale?: unknown
landScale?: unknown
workload?: unknown
hourly?: unknown
}
interface RateMethodRowLike {
rate?: unknown
budgetFee?: unknown
}
interface QuantityMethodRowLike {
id?: unknown
feeItem?: unknown
unit?: unknown
quantity?: unknown
unitPrice?: unknown
budgetFee?: number | null
remark?: unknown
}
interface ExportScaleRowLike {
majorid: number
major: number
cost: number | null
area: number | null
}
interface ExportServiceCoeLike {
serviceid: number
coe: number
remark: string
}
interface ExportMajorCoeLike {
majorid: number
coe: number
remark: string
}
interface ExportMethod1DetailLike {
proNum: number
major: number
cost: number
basicFee: number
basicFormula: string
basicFee_basic: number
optionalFormula: string
basicFee_optional: number
serviceCoe: number
majorCoe: number
processCoe: number
proportion: number
fee: number
remark: string
}
interface ExportMethod2DetailLike {
proNum: number
major: number
area: number
basicFee: number
basicFormula: string
basicFee_basic: number
optionalFormula: string
basicFee_optional: number
serviceCoe: number
majorCoe: number
processCoe: number
proportion: number
fee: number
remark: string
}
interface ExportMethod3DetailLike {
task: number
price: number
amount: number
basicFee: number
serviceCoe: number
fee: number
remark: string
}
interface ExportMethod4DetailLike {
expert: number
price: number
person_num: number
work_day: number
fee: number
remark: string
}
interface ExportMethod5DetailLike {
name: string
unit: string
amount: number
price: number
fee: number
remark: string
}
export const toSafeInteger = (value: unknown): number | null => {
const num = Number(value)
if (!Number.isInteger(num)) return null
if (!Number.isSafeInteger(num)) return null
return num
}
export const sumNumbers = (values: Array<number | null | undefined>): number =>
values.reduce<number>(
(sum, value) => sum + (typeof value === 'number' && Number.isFinite(value) ? value : 0),
0
)
export const toMoney = (value: unknown): number => roundTo(toFiniteNumber(value) ?? 0, 2)
export const isNonEmptyString = (value: unknown): value is string =>
typeof value === 'string' && value.trim().length > 0
export const getTaskIdFromRowId = (value: string): number | null => {
const match = /^task-(\d+)-\d+$/.exec(value)
return match ? toSafeInteger(match[1]) : null
}
export const getExpertIdFromRowId = (value: string): number | null => {
const match = /^expert-(\d+)$/.exec(value)
return match ? toSafeInteger(match[1]) : null
}
export const hasServiceId = (serviceId: string) =>
Object.prototype.hasOwnProperty.call(serviceList as Record<string, unknown>, serviceId)
export const sortServiceIdsByDict = (ids: string[]) =>
[...ids].sort((left, right) => {
const leftOrder = Number((serviceList as Record<string, any>)[left]?.order)
const rightOrder = Number((serviceList as Record<string, any>)[right]?.order)
const safeLeft = Number.isFinite(leftOrder) ? leftOrder : Number.MAX_SAFE_INTEGER
const safeRight = Number.isFinite(rightOrder) ? rightOrder : Number.MAX_SAFE_INTEGER
return safeLeft - safeRight
})
export const mapIndustryCodeToExportIndustry = (value: unknown): number => {
const raw = typeof value === 'string' ? value.trim().toUpperCase() : ''
if (!raw) return 0
if (raw === '0' || raw === 'E2') return 0
if (raw === '1' || raw === 'E3') return 1
if (raw === '2' || raw === 'E4') return 2
return 0
}
export const parseScaleScopedRowId = (rowId: unknown) => {
const raw = String(rowId || '').trim()
const scopedMatch = /^(\d+)::(.+)$/.exec(raw)
if (scopedMatch) {
return {
proNum: toSafeInteger(scopedMatch[1]) ?? 1,
majorPart: String(scopedMatch[2] || '').trim()
}
}
return {
proNum: 1,
majorPart: raw
}
}
export const toScaleMajorId = (row: ScaleMethodRowLike): number | null => {
const direct = toSafeInteger((row as { majorDictId?: unknown }).majorDictId)
if (direct != null) return direct
const parsed = parseScaleScopedRowId(row.id)
return toSafeInteger(parsed.majorPart)
}
export const toScaleProNum = (row: ScaleMethodRowLike): number => {
const parsed = parseScaleScopedRowId(row.id)
return parsed.proNum > 0 ? parsed.proNum : 1
}
export const normalizeTaskText = (value: unknown): string => String(value || '').trim()
export const resolveTaskRowServiceId = (row: WorkContentRowLike): number | null =>
toSafeInteger((row as { serviceid?: unknown })?.serviceid)
export const resolveScaleMethodFee = (row: ScaleMethodRowLike, mode: 'cost' | 'area') => {
const scaleValue = mode === 'cost' ? toFiniteNumber(row.amount) : toFiniteNumber(row.landArea)
const benchmarkSplit = getBenchmarkBudgetSplitByScale(scaleValue, mode)
const basicChecked = row.benchmarkBudgetBasicChecked !== false
const optionalChecked = row.benchmarkBudgetOptionalChecked !== false
const allUnchecked = !basicChecked && !optionalChecked
const benchmarkBudgetBasic = benchmarkSplit ? (basicChecked ? benchmarkSplit.basic : 0) : null
const benchmarkBudgetOptional = benchmarkSplit ? (optionalChecked ? benchmarkSplit.optional : 0) : null
const computedSplit = benchmarkSplit
? getScaleBudgetFeeSplit({
benchmarkBudgetBasic,
benchmarkBudgetOptional,
majorFactor: row.majorFactor,
consultCategoryFactor: row.consultCategoryFactor,
workStageFactor: row.workStageFactor,
workRatio: row.workRatio
})
: null
const basicFee = allUnchecked ? null : (toFiniteNumber(row.budgetFee) ?? computedSplit?.total ?? null)
const basicFeeBasic = allUnchecked ? null : (toFiniteNumber(row.budgetFeeBasic) ?? computedSplit?.basic ?? null)
const basicFeeOptional = allUnchecked ? null : (toFiniteNumber(row.budgetFeeOptional) ?? computedSplit?.optional ?? null)
const basicFormula = typeof row.basicFormula === 'string' && row.basicFormula.trim()
? row.basicFormula
: (basicChecked ? (benchmarkSplit?.basicFormula ?? '') : '')
const optionalFormula = typeof row.optionalFormula === 'string' && row.optionalFormula.trim()
? row.optionalFormula
: (optionalChecked ? (benchmarkSplit?.optionalFormula ?? '') : '')
return {
basicFee,
basicFeeBasic,
basicFeeOptional,
basicFormula,
optionalFormula
}
}
export interface ExportTaskGroupLike {
serviceid?: number
text: string[]
}
export const groupWorkContentTasks = (
rows: WorkContentRowLike[] | undefined,
options?: { forceUngroup?: boolean }
): ExportTaskGroupLike[] => {
const source = Array.isArray(rows) ? rows : []
const selected = source.filter(item => {
if (item && item.isAddTrigger === true) return false
const isCustom = Boolean(item?.custom)
const isChecked = Boolean(item?.checked)
return isCustom || isChecked
})
if (selected.length === 0) return []
const hasGroup = !options?.forceUngroup && selected.some(item => resolveTaskRowServiceId(item) != null)
if (!hasGroup) {
const text = selected
.map(item => normalizeTaskText(item?.content))
.filter(Boolean)
return text.length > 0 ? [{ text }] : []
}
const grouped = new Map<number, string[]>()
const orderedServiceIds: number[] = []
const ungroupedText: string[] = []
for (const item of selected) {
const content = normalizeTaskText(item?.content)
if (!content) continue
const serviceid = resolveTaskRowServiceId(item)
if (serviceid == null) {
ungroupedText.push(content)
continue
}
if (!grouped.has(serviceid)) {
grouped.set(serviceid, [])
orderedServiceIds.push(serviceid)
}
grouped.get(serviceid)?.push(content)
}
const groupedTasks: ExportTaskGroupLike[] = []
for (const serviceid of orderedServiceIds) {
const text = grouped.get(serviceid) || []
if (text.length === 0) continue
groupedTasks.push({ serviceid, text })
}
if (ungroupedText.length > 0) {
groupedTasks.push({ text: ungroupedText })
}
return groupedTasks
}
export const buildProjectServiceCoes = (rows: FactorRowLike[] | undefined) => {
if (!Array.isArray(rows)) return []
return rows
.map(row => {
const serviceid = toSafeInteger(row.id)
if (serviceid == null || row.budgetValue == null) return null
const coe = toFiniteNumber(row.budgetValue)
return {
serviceid,
coe: coe ?? 0,
remark: typeof row.remark === 'string' ? row.remark : ''
}
})
.filter((item): item is ExportServiceCoeLike => Boolean(item))
}
export const buildProjectMajorCoes = (rows: FactorRowLike[] | undefined) => {
if (!Array.isArray(rows)) return []
return rows
.map(row => {
const majorid = toSafeInteger(row.id)
if (majorid == null || row.budgetValue == null) return null
const coe = toFiniteNumber(row.budgetValue)
return {
majorid,
coe: coe ?? 0,
remark: typeof row.remark === 'string' ? row.remark : ''
}
})
.filter((item): item is ExportMajorCoeLike => Boolean(item))
}
export const toExportScaleRows = (rows: ScaleRowLike[] | undefined) => {
if (!Array.isArray(rows)) return []
return rows
.map(row => {
const majorid = toSafeInteger(row.id)
if (majorid == null || (row.amount == null && row.landArea == null)) return null
return {
majorid,
major: majorid,
cost: row.amount,
area: row.landArea
}
})
.filter((item): item is ExportScaleRowLike => Boolean(item))
}
export const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined) => {
if (!Array.isArray(rows)) return null
let hasTotalValue = false
const proSet = new Set<number>()
const det = rows
.map(row => {
const major = toScaleMajorId(row)
if (major == null) return null
const proNum = toScaleProNum(row)
proSet.add(proNum)
const cost = toFiniteNumber(row.amount)
const feeResolved = resolveScaleMethodFee(row, 'cost')
const basicFee = feeResolved.basicFee
if (basicFee != null) hasTotalValue = true
const basicFeeBasic = feeResolved.basicFeeBasic
const basicFeeOptional = feeResolved.basicFeeOptional
const remark = typeof row.remark === 'string' ? row.remark : ''
if (basicFee == null) return null
return {
proNum,
major,
cost: cost ?? 0,
basicFee: toMoney(basicFee),
basicFormula: feeResolved.basicFormula,
basicFee_basic: toMoney(basicFeeBasic),
optionalFormula: feeResolved.optionalFormula,
basicFee_optional: toMoney(basicFeeOptional),
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
majorCoe: toFiniteNumberOrZero(row.majorFactor),
processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
proportion: toFiniteNumber(row.workRatio) ?? 1,
fee: toMoney(basicFee),
remark
}
})
.filter((item): item is ExportMethod1DetailLike => Boolean(item))
if (det.length === 0 || !hasTotalValue) return null
return {
proAmount: proSet.size > 0 ? proSet.size : 1,
cost: sumNumbers(det.map(item => item.cost)),
basicFee: toMoney(sumNumbers(det.map(item => item.basicFee))),
basicFee_basic: toMoney(sumNumbers(det.map(item => item.basicFee_basic))),
basicFee_optional: toMoney(sumNumbers(det.map(item => item.basicFee_optional))),
fee: toMoney(sumNumbers(det.map(item => item.fee))),
det
}
}
export const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined) => {
if (!Array.isArray(rows)) return null
let hasTotalValue = false
const proSet = new Set<number>()
const det = rows
.map(row => {
const major = toScaleMajorId(row)
if (major == null) return null
const proNum = toScaleProNum(row)
proSet.add(proNum)
const area = toFiniteNumber(row.landArea)
const feeResolved = resolveScaleMethodFee(row, 'area')
const basicFee = feeResolved.basicFee
if (basicFee != null) hasTotalValue = true
const basicFeeBasic = feeResolved.basicFeeBasic
const basicFeeOptional = feeResolved.basicFeeOptional
const remark = typeof row.remark === 'string' ? row.remark : ''
if (basicFee == null) return null
return {
proNum,
major,
area: area ?? 0,
basicFee: toMoney(basicFee),
basicFormula: feeResolved.basicFormula,
basicFee_basic: toMoney(basicFeeBasic),
optionalFormula: feeResolved.optionalFormula,
basicFee_optional: toMoney(basicFeeOptional),
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
majorCoe: toFiniteNumberOrZero(row.majorFactor),
processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
proportion: toFiniteNumber(row.workRatio) ?? 1,
fee: toMoney(basicFee),
remark
}
})
.filter((item): item is ExportMethod2DetailLike => Boolean(item))
if (det.length === 0 || !hasTotalValue) return null
return {
proAmount: proSet.size > 0 ? proSet.size : 1,
area: sumNumbers(det.map(item => item.area)),
basicFee: toMoney(sumNumbers(det.map(item => item.basicFee))),
basicFee_basic: toMoney(sumNumbers(det.map(item => item.basicFee_basic))),
basicFee_optional: toMoney(sumNumbers(det.map(item => item.basicFee_optional))),
fee: toMoney(sumNumbers(det.map(item => item.fee))),
det
}
}
export const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined) => {
if (!Array.isArray(rows)) return null
let hasTotalValue = false
const det = rows
.map(row => {
const task = getTaskIdFromRowId(row.id)
if (task == null || row.basicFee == null) return null
const amount = toFiniteNumber(row.workload)
const basicFee = toFiniteNumber(row.basicFee)
const fee = toFiniteNumber(row.serviceFee)
if (fee != null) hasTotalValue = true
const remark = typeof row.remark === 'string' ? row.remark : ''
const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark)
if (!hasValue) return null
return {
task,
price: toFiniteNumberOrZero(row.budgetAdoptedUnitPrice),
amount: amount ?? 0,
basicFee: basicFee ?? 0,
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
fee: fee ?? 0,
remark
}
})
.filter((item): item is ExportMethod3DetailLike => Boolean(item))
if (det.length === 0 || !hasTotalValue) return null
return {
basicFee: sumNumbers(det.map(item => item.basicFee)),
fee: sumNumbers(det.map(item => item.fee)),
det
}
}
export const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined) => {
if (!Array.isArray(rows)) return null
let hasTotalValue = false
const det = rows
.map(row => {
const expert = getExpertIdFromRowId(row.id)
if (expert == null || row.serviceBudget == null) return null
const personNum = toFiniteNumber(row.personnelCount)
const workDay = toFiniteNumber(row.workdayCount)
const fee = toFiniteNumber(row.serviceBudget)
if (fee != null) hasTotalValue = true
const remark = typeof row.remark === 'string' ? row.remark : ''
const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark)
if (!hasValue) return null
return {
expert,
price: toFiniteNumberOrZero(row.adoptedBudgetUnitPrice),
person_num: personNum ?? 0,
work_day: workDay ?? 0,
fee: fee ?? 0,
remark
}
})
.filter((item): item is ExportMethod4DetailLike => Boolean(item))
if (det.length === 0 || !hasTotalValue) return null
return {
person_num: sumNumbers(det.map(item => item.person_num)),
work_day: sumNumbers(det.map(item => item.work_day)),
fee: sumNumbers(det.map(item => item.fee)),
det
}
}
export const buildServiceFee = (
row: ZxFwRowLike | null | undefined,
method1: { fee: number } | null,
method2: { fee: number } | null,
method3: { fee: number } | null,
method4: { fee: number } | null
) => {
const subtotal = toFiniteNumber(row?.subtotal)
return subtotal != null ? toMoney(subtotal) : 0
}
export const buildMethod0 = (payload: RateMethodRowLike | null | undefined) => {
const coe = toFiniteNumber(payload?.rate)
const fee = toFiniteNumber(payload?.budgetFee)
if (fee == null) return null
return {
coe: coe ?? 0,
fee
}
}
export const buildMethod5 = (rows: QuantityMethodRowLike[] | undefined) => {
if (!Array.isArray(rows) || rows.length === 0) return null
const subtotalRow = rows.find(row => String(row?.id || '') === 'fee-subtotal-fixed')
const subtotalFee = toFiniteNumber(subtotalRow?.budgetFee)
if (subtotalFee == null) return null
const det = rows
.filter(row => String(row?.id || '') !== 'fee-subtotal-fixed')
.map(row => {
const quantity = toFiniteNumber(row.quantity)
const unitPrice = toFiniteNumber(row.unitPrice)
const fee = toFiniteNumber(row.budgetFee)
const name = typeof row.feeItem === 'string' ? row.feeItem : ''
const unit = typeof row.unit === 'string' ? row.unit : ''
const remark = typeof row.remark === 'string' ? row.remark : ''
if (row.budgetFee == null) return null
return {
name,
unit,
amount: quantity ?? 0,
price: unitPrice ?? 0,
fee: fee ?? 0,
remark
}
})
.filter((item): item is ExportMethod5DetailLike => Boolean(item))
if (det.length === 0) return null
return {
fee: subtotalFee,
det
}
}
export const buildServiceFinalFee = (
row: ZxFwRowLike | null | undefined,
method1: { fee: number } | null,
method2: { fee: number } | null,
method3: { fee: number } | null,
method4: { fee: number } | null
) => {
const finalFee = toFiniteNumber(row?.finalFee)
if (finalFee != null) return toMoney(finalFee)
const subtotal = toFiniteNumber(row?.subtotal)
return subtotal != null ? toMoney(subtotal) : 0
}