diff --git a/src/components/pricing/InvestmentScalePricingPane.vue b/src/components/pricing/InvestmentScalePricingPane.vue index fe35a74..7d8e8e8 100644 --- a/src/components/pricing/InvestmentScalePricingPane.vue +++ b/src/components/pricing/InvestmentScalePricingPane.vue @@ -616,6 +616,24 @@ const formatReadonlyMoney = (params: any) => { type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked' +const updateBudgetCheckState = (rowId: string, checkField: BudgetCheckField, checked: boolean) => { + detailRows.value = detailRows.value.map(row => { + if (row.id !== rowId) return row + if (checkField === 'benchmarkBudgetBasicChecked') { + return { + ...row, + benchmarkBudgetBasicChecked: checked, + benchmarkBudgetBasic: checked ? row.benchmarkBudgetBasic : 0 + } + } + return { + ...row, + benchmarkBudgetOptionalChecked: checked, + benchmarkBudgetOptional: checked ? row.benchmarkBudgetOptional : 0 + } + }) +} + const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => { const valueText = formatReadonlyMoney(params) const hasValue = params.value != null && params.value !== '' @@ -635,20 +653,13 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par checkbox.className = 'cursor-pointer' checkbox.checked = params.data[checkField] !== false + checkbox.addEventListener('mousedown', event => event.stopPropagation()) checkbox.addEventListener('click', event => event.stopPropagation()) checkbox.addEventListener('change', () => { const targetRow = params.data as DetailRow | undefined if (!targetRow) return - targetRow[checkField] = checkbox.checked - params.node?.setDataValue?.(checkField, checkbox.checked) - - if (!checkbox.checked) { - const budgetField = checkField === 'benchmarkBudgetBasicChecked' ? 'benchmarkBudgetBasic' : 'benchmarkBudgetOptional' - targetRow[budgetField] = 0 - params.node?.setDataValue?.(budgetField, 0) - } - + updateBudgetCheckState(targetRow.id, checkField, checkbox.checked) handleCellValueChanged() params.api?.refreshCells?.({ rowNodes: params.node ? [params.node] : undefined, diff --git a/src/components/pricing/LandScalePricingPane.vue b/src/components/pricing/LandScalePricingPane.vue index 1b5daff..58235b4 100644 --- a/src/components/pricing/LandScalePricingPane.vue +++ b/src/components/pricing/LandScalePricingPane.vue @@ -481,6 +481,24 @@ const formatReadonlyMoney = (params: any) => { type BudgetCheckField = 'benchmarkBudgetBasicChecked' | 'benchmarkBudgetOptionalChecked' +const updateBudgetCheckState = (rowId: string, checkField: BudgetCheckField, checked: boolean) => { + detailRows.value = detailRows.value.map(row => { + if (row.id !== rowId) return row + if (checkField === 'benchmarkBudgetBasicChecked') { + return { + ...row, + benchmarkBudgetBasicChecked: checked, + benchmarkBudgetBasic: checked ? row.benchmarkBudgetBasic : 0 + } + } + return { + ...row, + benchmarkBudgetOptionalChecked: checked, + benchmarkBudgetOptional: checked ? row.benchmarkBudgetOptional : 0 + } + }) +} + const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (params: any) => { const valueText = formatReadonlyMoney(params) const hasValue = params.value != null && params.value !== '' @@ -499,14 +517,17 @@ const createBudgetCellRendererWithCheck = (checkField: BudgetCheckField) => (par checkbox.type = 'checkbox' checkbox.className = 'cursor-pointer' checkbox.checked = params.data[checkField] !== false + checkbox.addEventListener('mousedown', event => event.stopPropagation()) checkbox.addEventListener('click', event => event.stopPropagation()) checkbox.addEventListener('change', () => { - params.data[checkField] = checkbox.checked - if (!checkbox.checked) { - const budgetField = checkField === 'benchmarkBudgetBasicChecked' ? 'benchmarkBudgetBasic' : 'benchmarkBudgetOptional' - params.data[budgetField] = 0 - } + const targetRow = params.data as DetailRow | undefined + if (!targetRow) return + updateBudgetCheckState(targetRow.id, checkField, checkbox.checked) handleCellValueChanged() + params.api?.refreshCells?.({ + rowNodes: params.node ? [params.node] : undefined, + force: true + }) }) const valueSpan = document.createElement('span') diff --git a/src/lib/pricingMethodTotals.ts b/src/lib/pricingMethodTotals.ts index a044b00..dc8c11b 100644 --- a/src/lib/pricingMethodTotals.ts +++ b/src/lib/pricingMethodTotals.ts @@ -7,7 +7,7 @@ import { } from '@/sql' import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal' import { toFiniteNumberOrNull } from '@/lib/number' -import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee' +import { getBenchmarkBudgetByScale, getBenchmarkBudgetSplitByScale, getScaleBudgetFee, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee' import { useZxFwPricingStore, type ServicePricingMethod } from '@/pinia/zxFwPricing' import { useKvStore } from '@/pinia/kv' @@ -40,6 +40,8 @@ interface ScaleRow { id: string amount: number | null landArea: number | null + benchmarkBudgetBasicChecked: boolean + benchmarkBudgetOptionalChecked: boolean consultCategoryFactor: number | null majorFactor: number | null workStageFactor: number | null @@ -313,6 +315,8 @@ const buildDefaultScaleRows = ( id, amount: null, landArea: null, + benchmarkBudgetBasicChecked: true, + benchmarkBudgetOptionalChecked: true, consultCategoryFactor: defaultConsultCategoryFactor, majorFactor: majorFactorMap?.get(id) ?? getDefaultMajorFactorById(id), workStageFactor: 1, @@ -351,6 +355,14 @@ const mergeScaleRows = ( ...row, amount: toFiniteNumberOrNull(fromDb.amount), landArea: toFiniteNumberOrNull(fromDb.landArea), + benchmarkBudgetBasicChecked: + typeof (fromDb as { benchmarkBudgetBasicChecked?: unknown }).benchmarkBudgetBasicChecked === 'boolean' + ? Boolean((fromDb as { benchmarkBudgetBasicChecked?: unknown }).benchmarkBudgetBasicChecked) + : true, + benchmarkBudgetOptionalChecked: + typeof (fromDb as { benchmarkBudgetOptionalChecked?: unknown }).benchmarkBudgetOptionalChecked === 'boolean' + ? Boolean((fromDb as { benchmarkBudgetOptionalChecked?: unknown }).benchmarkBudgetOptionalChecked) + : true, consultCategoryFactor: toFiniteNumberOrNull(fromDb.consultCategoryFactor) ?? (hasConsultCategoryFactor ? null : defaultConsultCategoryFactor), @@ -373,14 +385,32 @@ const getBenchmarkBudgetByAmount = (amount: MaybeNumber) => const getBenchmarkBudgetByLandArea = (landArea: MaybeNumber) => getBenchmarkBudgetByScale(landArea, 'area') +const getCheckedScaleBudgetSplit = ( + value: MaybeNumber, + mode: 'cost' | 'area', + row: Pick +) => { + const split = getBenchmarkBudgetSplitByScale(value, mode) + if (!split) return null + const basic = row.benchmarkBudgetBasicChecked === false ? 0 : split.basic + const optional = row.benchmarkBudgetOptionalChecked === false ? 0 : split.optional + return { + basic, + optional + } +} + const getInvestmentBudgetFee = (row: ScaleRow) => { - return getScaleBudgetFee({ - benchmarkBudget: getBenchmarkBudgetByAmount(row.amount), + const split = getCheckedScaleBudgetSplit(row.amount, 'cost', row) + if (!split) return null + return getScaleBudgetFeeSplit({ + benchmarkBudgetBasic: split.basic, + benchmarkBudgetOptional: split.optional, majorFactor: row.majorFactor, consultCategoryFactor: row.consultCategoryFactor, workStageFactor: row.workStageFactor, workRatio: row.workRatio - }) + })?.total ?? null } const getOnlyCostScaleBudgetFee = ( @@ -409,13 +439,19 @@ const getOnlyCostScaleBudgetFee = ( return sumByNumberNullable(sourceRows, row => { const amount = toFiniteNumberOrNull(row?.amount) if (amount == null) return null - return getScaleBudgetFee({ - benchmarkBudget: getBenchmarkBudgetByAmount(amount), + const split = getCheckedScaleBudgetSplit(amount, 'cost', { + benchmarkBudgetBasicChecked: typeof row?.benchmarkBudgetBasicChecked === 'boolean' ? row.benchmarkBudgetBasicChecked : true, + benchmarkBudgetOptionalChecked: typeof row?.benchmarkBudgetOptionalChecked === 'boolean' ? row.benchmarkBudgetOptionalChecked : true + }) + if (!split) return null + return getScaleBudgetFeeSplit({ + benchmarkBudgetBasic: split.basic, + benchmarkBudgetOptional: split.optional, majorFactor: getRowNumberOrFallback(row, 'majorFactor', defaultMajorFactor), consultCategoryFactor: getRowNumberOrFallback(row, 'consultCategoryFactor', defaultConsultCategoryFactor), workStageFactor: getRowNumberOrFallback(row, 'workStageFactor', 1), workRatio: getRowNumberOrFallback(row, 'workRatio', 100) - }) + })?.total ?? null }) } @@ -431,13 +467,19 @@ const getOnlyCostScaleBudgetFee = ( const majorFactor = getRowNumberOrFallback(onlyRow, 'majorFactor', defaultMajorFactor) const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1) const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 100) - return getScaleBudgetFee({ - benchmarkBudget: getBenchmarkBudgetByAmount(totalAmount), + const split = getCheckedScaleBudgetSplit(totalAmount, 'cost', { + benchmarkBudgetBasicChecked: typeof onlyRow?.benchmarkBudgetBasicChecked === 'boolean' ? onlyRow.benchmarkBudgetBasicChecked : true, + benchmarkBudgetOptionalChecked: typeof onlyRow?.benchmarkBudgetOptionalChecked === 'boolean' ? onlyRow.benchmarkBudgetOptionalChecked : true + }) + if (!split) return null + return getScaleBudgetFeeSplit({ + benchmarkBudgetBasic: split.basic, + benchmarkBudgetOptional: split.optional, majorFactor, consultCategoryFactor, workStageFactor, workRatio - }) + })?.total ?? null } const buildOnlyCostScaleDetailRows = ( @@ -475,6 +517,7 @@ const buildOnlyCostScaleDetailRows = ( { id: onlyCostRowId, amount: totalAmount, + landArea: null, consultCategoryFactor, majorFactor, workStageFactor, @@ -486,13 +529,16 @@ const buildOnlyCostScaleDetailRows = ( } const getLandBudgetFee = (row: ScaleRow) => { - return getScaleBudgetFee({ - benchmarkBudget: getBenchmarkBudgetByLandArea(row.landArea), + const split = getCheckedScaleBudgetSplit(row.landArea, 'area', row) + if (!split) return null + return getScaleBudgetFeeSplit({ + benchmarkBudgetBasic: split.basic, + benchmarkBudgetOptional: split.optional, majorFactor: row.majorFactor, consultCategoryFactor: row.consultCategoryFactor, workStageFactor: row.workStageFactor, workRatio: row.workRatio - }) + })?.total ?? null } const getTaskEntriesByServiceId = (serviceId: string | number) => @@ -650,6 +696,10 @@ const normalizeScopedScaleRows = ( id: resolvedMajorId, amount: toFiniteNumberOrNull(row.amount), landArea: toFiniteNumberOrNull(row.landArea), + benchmarkBudgetBasicChecked: + typeof row.benchmarkBudgetBasicChecked === 'boolean' ? row.benchmarkBudgetBasicChecked : true, + benchmarkBudgetOptionalChecked: + typeof row.benchmarkBudgetOptionalChecked === 'boolean' ? row.benchmarkBudgetOptionalChecked : true, consultCategoryFactor: toFiniteNumberOrNull(row.consultCategoryFactor) ?? (hasConsultCategoryFactor ? null : defaultConsultCategoryFactor),