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 => values.reduce( (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, serviceId) export const sortServiceIdsByDict = (ids: string[]) => [...ids].sort((left, right) => { const leftOrder = Number((serviceList as Record)[left]?.order) const rightOrder = Number((serviceList as Record)[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() 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() 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() 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 }