This commit is contained in:
wintsa 2026-03-11 17:41:08 +08:00
parent 3d26b0b259
commit 0f71fff9ac
11 changed files with 873 additions and 205 deletions

View File

@ -423,7 +423,17 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
const totalPersonnelCount = computed(() => sumByNumber(detailRows.value, row => row.personnelCount)) const totalPersonnelCount = computed(() => sumByNumber(detailRows.value, row => row.personnelCount))
const totalWorkdayCount = computed(() => sumByNumber(detailRows.value, row => row.workdayCount)) const totalWorkdayCount = computed(() => sumByNumber(detailRows.value, row => row.workdayCount))
const totalServiceBudget = computed(() => sumByNumber(detailRows.value, row => calcServiceBudget(row))) const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null => {
let hasValid = false
const total = sumByNumber(rows, row => {
const value = Number(pick(row))
if (!Number.isFinite(value)) return null
hasValid = true
return value
})
return hasValid ? total : null
}
const totalServiceBudget = computed(() => sumNullableBy(detailRows.value, row => calcServiceBudget(row)))
const pinnedTopRowData = computed(() => [ const pinnedTopRowData = computed(() => [
{ {
id: 'pinned-total-row', id: 'pinned-total-row',

View File

@ -59,6 +59,12 @@ interface XmBaseInfoState {
projectIndustry?: string projectIndustry?: string
} }
interface XmScaleState {
detailRows?: unknown[]
roughCalcEnabled?: boolean
totalAmount?: number | null
}
const STORAGE_KEY = 'ht-card-v1' const STORAGE_KEY = 'ht-card-v1'
const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw' const CONTRACT_SEGMENT_FILE_EXTENSION = '.htzw'
const CONTRACT_SEGMENT_VERSION = 2 const CONTRACT_SEGMENT_VERSION = 2
@ -68,6 +74,7 @@ const CONTRACT_CONSULT_FACTOR_KEY_PREFIX = 'ht-consult-category-factor-v1-'
const CONTRACT_MAJOR_FACTOR_KEY_PREFIX = 'ht-major-factor-v1-' const CONTRACT_MAJOR_FACTOR_KEY_PREFIX = 'ht-major-factor-v1-'
const PRICING_KEY_PREFIXES = ['tzGMF-', 'ydGMF-', 'gzlF-', 'hourlyPricing-', 'htExtraFee-'] const PRICING_KEY_PREFIXES = ['tzGMF-', 'ydGMF-', 'gzlF-', 'hourlyPricing-', 'htExtraFee-']
const PROJECT_INFO_KEY = 'xm-base-info-v1' const PROJECT_INFO_KEY = 'xm-base-info-v1'
const PROJECT_SCALE_KEY = 'xm-info-v3'
const SERVICE_PRICING_METHODS = ['investScale', 'landScale', 'workload', 'hourly'] as const const SERVICE_PRICING_METHODS = ['investScale', 'landScale', 'workload', 'hourly'] as const
@ -361,6 +368,19 @@ const saveContracts = async () => {
} }
} }
const initializeContractScaleData = async (contractId: string) => {
const source = await kvStore.getItem<XmScaleState>(PROJECT_SCALE_KEY)
const payload: XmScaleState = {
detailRows: Array.isArray(source?.detailRows) ? cloneJson(source.detailRows) : [],
roughCalcEnabled: Boolean(source?.roughCalcEnabled),
totalAmount:
typeof source?.totalAmount === 'number' && Number.isFinite(source.totalAmount)
? source.totalAmount
: null
}
await kvStore.setItem(`${CONTRACT_KEY_PREFIX}${contractId}`, payload)
}
const normalizeContractsFromPayload = (value: unknown): ContractItem[] => { const normalizeContractsFromPayload = (value: unknown): ContractItem[] => {
if (!Array.isArray(value)) return [] if (!Array.isArray(value)) return []
return value return value
@ -795,17 +815,20 @@ const createContract = async () => {
return return
} }
contracts.value = [ const newContract: ContractItem = {
...contracts.value,
{
id: `ct-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, id: `ct-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
name, name,
order: contracts.value.length, order: contracts.value.length,
createdAt: new Date().toISOString() createdAt: new Date().toISOString()
} }
] contracts.value = [...contracts.value, newContract]
await saveContracts() await saveContracts()
try {
await initializeContractScaleData(newContract.id)
} catch (error) {
console.error('initialize contract scale failed:', error)
}
notify('新建成功') notify('新建成功')
closeCreateModal() closeCreateModal()
await nextTick() await nextTick()

View File

@ -1,7 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { parseDate } from '@internationalized/date' import { parseDate } from '@internationalized/date'
import { onMounted, ref, watch } from 'vue' import { onMounted, ref, watch } from 'vue'
import { industryTypeList } from '@/sql' import {
getMajorDictEntries,
getServiceDictEntries,
getIndustryTypeValue,
industryTypeList,
isIndustryEnabledByType,
isMajorIdInIndustryScope
} from '@/sql'
import { useKvStore } from '@/pinia/kv' import { useKvStore } from '@/pinia/kv'
import { Calendar as CalendarIcon, CircleHelp } from 'lucide-vue-next' import { Calendar as CalendarIcon, CircleHelp } from 'lucide-vue-next'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip' import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
@ -38,8 +45,28 @@ interface XmInfoState {
} }
type MajorParentNode = { id: string; name: string } type MajorParentNode = { id: string; name: string }
type DictItemLite = {
code?: string
name?: string
defCoe?: number | null
notshowByzxflxs?: boolean
}
type FactorPersistRow = {
id: string
code: string
name: string
standardFactor: number | null
budgetValue: number | null
remark: string
path: string[]
}
type FactorPersistState = {
detailRows: FactorPersistRow[]
}
const DB_KEY = 'xm-base-info-v1' const DB_KEY = 'xm-base-info-v1'
const XM_CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
const XM_MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务' const DEFAULT_PROJECT_NAME = 'xxx造价咨询服务'
const INDUSTRY_HINT_TEXT = '变更需要重置后重新选择' const INDUSTRY_HINT_TEXT = '变更需要重置后重新选择'
const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed' const PROJECT_INIT_CHANGED_EVENT = 'xm-project-init-changed'
@ -101,6 +128,76 @@ const majorParentCodeSet = new Set(majorParentNodes.map(item => item.id))
const DEFAULT_PROJECT_INDUSTRY = majorParentNodes[0]?.id || '' const DEFAULT_PROJECT_INDUSTRY = majorParentNodes[0]?.id || ''
const kvStore = useKvStore() const kvStore = useKvStore()
const buildCodePath = (code: string, selfId: string, codeIdMap: Map<string, string>) => {
const parts = code.split('-').filter(Boolean)
if (!parts.length) return [selfId]
const path: string[] = []
let currentCode = parts[0]
const firstId = codeIdMap.get(currentCode)
if (firstId) path.push(firstId)
for (let i = 1; i < parts.length; i += 1) {
currentCode = `${currentCode}-${parts[i]}`
const id = codeIdMap.get(currentCode)
if (id) path.push(id)
}
if (!path.length || path[path.length - 1] !== selfId) path.push(selfId)
return path
}
const buildFactorRowsFromEntries = (entries: Array<{ id: string; item: DictItemLite }>): FactorPersistRow[] => {
const codeIdMap = new Map<string, string>()
for (const entry of entries) {
const code = String(entry.item?.code || '').trim()
if (!code) continue
codeIdMap.set(code, entry.id)
}
return entries
.map(entry => {
const code = String(entry.item?.code || '').trim()
const name = String(entry.item?.name || '').trim()
if (!code || !name) return null
const standardFactor =
typeof entry.item?.defCoe === 'number' && Number.isFinite(entry.item.defCoe)
? entry.item.defCoe
: null
return {
id: entry.id,
code,
name,
standardFactor,
budgetValue: standardFactor,
remark: '',
path: buildCodePath(code, entry.id, codeIdMap)
}
})
.filter((item): item is FactorPersistRow => Boolean(item))
}
const initializeProjectFactorStates = async (industry: string) => {
const industryType = getIndustryTypeValue(industry)
const consultEntries = getServiceDictEntries()
.map(({ id, item }) => ({ id, item: item as DictItemLite }))
.filter(({ item }) => {
if (item.notshowByzxflxs === true) return false
return isIndustryEnabledByType(item as Record<string, unknown>, industryType)
})
const majorEntries = getMajorDictEntries()
.map(({ id, item }) => ({ id, item: item as DictItemLite }))
.filter(({ id, item }) => item.notshowByzxflxs !== true && isMajorIdInIndustryScope(id, industry))
const consultPayload: FactorPersistState = {
detailRows: buildFactorRowsFromEntries(consultEntries)
}
const majorPayload: FactorPersistState = {
detailRows: buildFactorRowsFromEntries(majorEntries)
}
await Promise.all([
kvStore.setItem(XM_CONSULT_CATEGORY_FACTOR_KEY, consultPayload),
kvStore.setItem(XM_MAJOR_FACTOR_KEY, majorPayload)
])
}
const saveToIndexedDB = async () => { const saveToIndexedDB = async () => {
try { try {
const payload: XmInfoState = { const payload: XmInfoState = {
@ -195,6 +292,11 @@ const createProject = async () => {
isProjectInitialized.value = true isProjectInitialized.value = true
showCreateDialog.value = false showCreateDialog.value = false
await saveToIndexedDB() await saveToIndexedDB()
try {
await initializeProjectFactorStates(selectedIndustry)
} catch (error) {
console.error('initializeProjectFactorStates failed:', error)
}
window.dispatchEvent(new CustomEvent<boolean>(PROJECT_INIT_CHANGED_EVENT, { detail: true })) window.dispatchEvent(new CustomEvent<boolean>(PROJECT_INIT_CHANGED_EVENT, { detail: true }))
} }

View File

@ -582,6 +582,14 @@ const formatEditableNumber = (params: any) => {
return formatThousandsFlexible(params.value, 3) return formatThousandsFlexible(params.value, 3)
} }
const formatEditableRatio2 = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '请输入'
}
if (params.value == null) return ''
return formatThousandsFlexible(params.value, 2)
}
const formatConsultCategoryFactor = (params: any) => { const formatConsultCategoryFactor = (params: any) => {
return formatEditableNumber(params) return formatEditableNumber(params)
} }
@ -878,8 +886,8 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
}, },
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
valueFormatter: formatEditableNumber valueFormatter: formatEditableRatio2
}, },
{ {
headerName: '合计', headerName: '合计',
@ -960,9 +968,20 @@ const autoGroupColumnDef: ColDef = {
const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount)) const totalAmount = computed(() => sumByNumber(detailRows.value, row => row.amount))
const totalBudgetFeeBasic = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.basic)) const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null => {
const totalBudgetFeeOptional = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.optional)) let hasValid = false
const totalBudgetFee = computed(() => sumByNumber(detailRows.value, row => getBudgetFee(row))) const total = sumByNumber(rows, row => {
const value = Number(pick(row))
if (!Number.isFinite(value)) return null
hasValid = true
return value
})
return hasValid ? total : null
}
const totalBudgetFeeBasic = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
const totalBudgetFeeOptional = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
const totalBudgetFee = computed(() => sumNullableBy(detailRows.value, row => getBudgetFee(row)))
const pinnedTopRowData = computed(() => { const pinnedTopRowData = computed(() => {
return [ return [
{ {
@ -1284,16 +1303,14 @@ const processCellFromClipboard = (params: any) => {
<h3 class="text-sm font-semibold text-foreground">投资规模明细</h3> <h3 class="text-sm font-semibold text-foreground">投资规模明细</h3>
<div v-if="isMutipleService" class="flex items-center gap-2"> <div v-if="isMutipleService" class="flex items-center gap-2">
<span class="text-xs text-muted-foreground">项目数量</span> <span class="text-xs text-muted-foreground">项目数量</span>
<NumberFieldRoot <NumberFieldRoot v-model="projectCount" :min="1" :step="1"
v-model="projectCount"
:min="1"
:step="1"
class="inline-flex items-center rounded-md border bg-background" class="inline-flex items-center rounded-md border bg-background"
@update:model-value="value => void applyProjectCountChange(value)" @update:model-value="value => void applyProjectCountChange(value)">
> <NumberFieldDecrement class="cursor-pointer px-2 py-1 text-xs text-muted-foreground hover:bg-muted">-
<NumberFieldDecrement class="cursor-pointer px-2 py-1 text-xs text-muted-foreground hover:bg-muted">-</NumberFieldDecrement> </NumberFieldDecrement>
<NumberFieldInput class="h-7 w-14 border-x bg-transparent px-2 text-center text-xs outline-none" /> <NumberFieldInput class="h-7 w-14 border-x bg-transparent px-2 text-center text-xs outline-none" />
<NumberFieldIncrement class="cursor-pointer px-2 py-1 text-xs text-muted-foreground hover:bg-muted">+</NumberFieldIncrement> <NumberFieldIncrement class="cursor-pointer px-2 py-1 text-xs text-muted-foreground hover:bg-muted">+
</NumberFieldIncrement>
</NumberFieldRoot> </NumberFieldRoot>
</div> </div>
</div> </div>
@ -1304,7 +1321,8 @@ const processCellFromClipboard = (params: any) => {
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" /> <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
<AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl"> <AlertDialogContent
class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
<AlertDialogTitle class="text-base font-semibold">确认清空当前明细</AlertDialogTitle> <AlertDialogTitle class="text-base font-semibold">确认清空当前明细</AlertDialogTitle>
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground"> <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
将清空当前投资规模明细是否继续 将清空当前投资规模明细是否继续
@ -1326,7 +1344,8 @@ const processCellFromClipboard = (params: any) => {
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" /> <AlertDialogOverlay class="fixed inset-0 z-50 bg-black/45" />
<AlertDialogContent class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl"> <AlertDialogContent
class="fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border bg-background p-5 shadow-xl">
<AlertDialogTitle class="text-base font-semibold">确认覆盖当前明细</AlertDialogTitle> <AlertDialogTitle class="text-base font-semibold">确认覆盖当前明细</AlertDialogTitle>
<AlertDialogDescription class="mt-2 text-sm text-muted-foreground"> <AlertDialogDescription class="mt-2 text-sm text-muted-foreground">
将使用合同默认数据覆盖当前投资规模明细是否继续 将使用合同默认数据覆盖当前投资规模明细是否继续

View File

@ -457,6 +457,14 @@ const formatEditableNumber = (params: any) => {
return formatThousandsFlexible(params.value, 3) return formatThousandsFlexible(params.value, 3)
} }
const formatEditableRatio2 = (params: any) => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return '请输入'
}
if (params.value == null) return ''
return formatThousandsFlexible(params.value, 2)
}
const formatConsultCategoryFactor = (params: any) => { const formatConsultCategoryFactor = (params: any) => {
return formatEditableNumber(params) return formatEditableNumber(params)
} }
@ -731,8 +739,8 @@ const columnDefs: Array<ColDef<DetailRow> | ColGroupDef<DetailRow>> = [
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
}, },
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueParser: params => parseNumberOrNull(params.newValue, { precision: 2 }),
valueFormatter: formatEditableNumber valueFormatter: formatEditableRatio2
}, },
{ {
headerName: '合计', headerName: '合计',
@ -814,9 +822,20 @@ const totalBenchmarkBudgetBasic = computed(() => sumByNumber(detailRows.value, r
const totalBenchmarkBudgetOptional = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByLandArea(row)?.optional)) const totalBenchmarkBudgetOptional = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByLandArea(row)?.optional))
const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByLandArea(row)?.total)) const totalBenchmarkBudget = computed(() => sumByNumber(detailRows.value, row => getBenchmarkBudgetSplitByLandArea(row)?.total))
const totalBudgetFeeBasic = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.basic)) const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null => {
const totalBudgetFeeOptional = computed(() => sumByNumber(detailRows.value, row => getBudgetFeeSplit(row)?.optional)) let hasValid = false
const totalBudgetFee = computed(() => sumByNumber(detailRows.value, row => getBudgetFee(row))) const total = sumByNumber(rows, row => {
const value = Number(pick(row))
if (!Number.isFinite(value)) return null
hasValid = true
return value
})
return hasValid ? total : null
}
const totalBudgetFeeBasic = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.basic))
const totalBudgetFeeOptional = computed(() => sumNullableBy(detailRows.value, row => getBudgetFeeSplit(row)?.optional))
const totalBudgetFee = computed(() => sumNullableBy(detailRows.value, row => getBudgetFee(row)))
const pinnedTopRowData = computed(() => [ const pinnedTopRowData = computed(() => [
{ {
id: 'pinned-total-row', id: 'pinned-total-row',

View File

@ -400,7 +400,18 @@ const columnDefs: ColDef<DetailRow>[] = [
const totalWorkload = computed(() => sumByNumber(detailRows.value, row => row.workload)) const totalWorkload = computed(() => sumByNumber(detailRows.value, row => row.workload))
const totalBasicFee = computed(() => sumByNumber(detailRows.value, row => calcBasicFee(row))) const totalBasicFee = computed(() => sumByNumber(detailRows.value, row => calcBasicFee(row)))
const totalServiceFee = computed(() => sumByNumber(detailRows.value, row => calcServiceFee(row))) const sumNullableBy = <T>(rows: T[], pick: (row: T) => unknown): number | null => {
let hasValid = false
const total = sumByNumber(rows, row => {
const value = Number(pick(row))
if (!Number.isFinite(value)) return null
hasValid = true
return value
})
return hasValid ? total : null
}
const totalServiceFee = computed(() => sumNullableBy(detailRows.value, row => calcServiceFee(row)))
const pinnedTopRowData = computed(() => [ const pinnedTopRowData = computed(() => [
{ {
id: 'pinned-total-row', id: 'pinned-total-row',

View File

@ -2,7 +2,7 @@
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { ComponentPublicInstance, PropType } from 'vue' import type { ComponentPublicInstance, PropType } from 'vue'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
import type { ColDef, GridOptions, ICellRendererParams } from 'ag-grid-community' import type { ColDef, GridOptions, ICellRendererParams, IHeaderParams } from 'ag-grid-community'
import { myTheme, gridOptions } from '@/lib/diyAgGridOptions' import { myTheme, gridOptions } from '@/lib/diyAgGridOptions'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale' import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { addNumbers } from '@/lib/decimal' import { addNumbers } from '@/lib/decimal'
@ -14,7 +14,7 @@ import {
getPricingMethodTotalsForServices, getPricingMethodTotalsForServices,
type PricingMethodTotals type PricingMethodTotals
} from '@/lib/pricingMethodTotals' } from '@/lib/pricingMethodTotals'
import { Pencil, Eraser, Trash2 } from 'lucide-vue-next' import { Pencil, Eraser, Trash2, CircleHelp } from 'lucide-vue-next'
import { import {
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
@ -45,6 +45,7 @@ interface DetailRow {
id: string id: string
code: string code: string
name: string name: string
process: number | null
investScale: number | null investScale: number | null
landScale: number | null landScale: number | null
workload: number | null workload: number | null
@ -146,6 +147,7 @@ const detailRows = computed<DetailRow[]>(() =>
id: String(row.id || ''), id: String(row.id || ''),
code: row.code || '', code: row.code || '',
name: row.name || '', name: row.name || '',
process: String(row.id || '') === fixedBudgetRow.id ? null : (Number(row.process) === 1 ? 1 : 0),
investScale: typeof row.investScale === 'number' ? row.investScale : null, investScale: typeof row.investScale === 'number' ? row.investScale : null,
landScale: typeof row.landScale === 'number' ? row.landScale : null, landScale: typeof row.landScale === 'number' ? row.landScale : null,
workload: typeof row.workload === 'number' ? row.workload : null, workload: typeof row.workload === 'number' ? row.workload : null,
@ -165,6 +167,7 @@ const getCurrentContractState = (): ZxFwViewState => {
id: String(row.id || ''), id: String(row.id || ''),
code: row.code || '', code: row.code || '',
name: row.name || '', name: row.name || '',
process: String(row.id || '') === fixedBudgetRow.id ? null : (Number(row.process) === 1 ? 1 : 0),
investScale: typeof row.investScale === 'number' ? row.investScale : null, investScale: typeof row.investScale === 'number' ? row.investScale : null,
landScale: typeof row.landScale === 'number' ? row.landScale : null, landScale: typeof row.landScale === 'number' ? row.landScale : null,
workload: typeof row.workload === 'number' ? row.workload : null, workload: typeof row.workload === 'number' ? row.workload : null,
@ -354,7 +357,14 @@ const numericParser = (newValue: any): number | null => {
return parseNumberOrNull(newValue, { precision: 3 }) return parseNumberOrNull(newValue, { precision: 3 })
} }
const valueOrZero = (v: number | null | undefined) => (typeof v === 'number' ? v : 0) const isFiniteNumberValue = (value: unknown): value is number =>
typeof value === 'number' && Number.isFinite(value)
const sumNullableNumbers = (values: Array<number | null | undefined>): number | null => {
const validValues = values.filter(isFiniteNumberValue)
if (validValues.length === 0) return null
return addNumbers(...validValues)
}
const getServiceMethodTypeById = (serviceId: string) => { const getServiceMethodTypeById = (serviceId: string) => {
const type = serviceById.value.get(serviceId)?.type const type = serviceById.value.get(serviceId)?.type
@ -393,22 +403,22 @@ const sanitizePricingFieldsByService = (
const getMethodTotalFromRows = ( const getMethodTotalFromRows = (
rows: DetailRow[], rows: DetailRow[],
field: 'investScale' | 'landScale' | 'workload' | 'hourly' field: 'investScale' | 'landScale' | 'workload' | 'hourly'
) => ) => sumNullableNumbers(
rows.reduce((sum, row) => { rows
if (isFixedRow(row)) return sum .filter(row => !isFixedRow(row))
return addNumbers(sum, valueOrZero(row[field])) .map(row => row[field])
}, 0) )
const getMethodTotal = (field: 'investScale' | 'landScale' | 'workload' | 'hourly') => const getMethodTotal = (field: 'investScale' | 'landScale' | 'workload' | 'hourly') =>
getMethodTotalFromRows(detailRows.value, field) getMethodTotalFromRows(detailRows.value, field)
const getFixedRowSubtotal = () => const getFixedRowSubtotal = () =>
addNumbers( sumNullableNumbers([
getMethodTotal('investScale'), getMethodTotal('investScale'),
getMethodTotal('landScale'), getMethodTotal('landScale'),
getMethodTotal('workload'), getMethodTotal('workload'),
getMethodTotal('hourly') getMethodTotal('hourly')
) ])
const getPricingPaneStorageKeys = (serviceId: string) => const getPricingPaneStorageKeys = (serviceId: string) =>
zxFwPricingStore.getServicePricingStorageKeys(props.contractId, serviceId) zxFwPricingStore.getServicePricingStorageKeys(props.contractId, serviceId)
@ -469,7 +479,7 @@ const clearRowValues = async (row: DetailRow) => {
landScale: nextLandScale, landScale: nextLandScale,
workload: nextWorkload, workload: nextWorkload,
hourly: nextHourly, hourly: nextHourly,
subtotal: addNumbers(nextInvestScale, nextLandScale, nextWorkload, nextHourly) subtotal: sumNullableNumbers([nextInvestScale, nextLandScale, nextWorkload, nextHourly])
} }
: item : item
) )
@ -532,6 +542,60 @@ const ActionCellRenderer = defineComponent({
} }
}) })
const ProcessCellRenderer = defineComponent({
name: 'ProcessCellRenderer',
props: {
params: {
type: Object as PropType<ICellRendererParams<DetailRow>>,
required: true
}
},
setup(props) {
return () => {
const row = props.params.data
if (!row || isFixedRow(row)) return null
const checked = row.process === 1
const onToggle = (event: Event) => {
event.stopPropagation()
const target = event.target as HTMLInputElement | null
void props.params.context?.onToggleProcess?.(row.id, Boolean(target?.checked))
}
return h('div', { class: 'flex items-center justify-center w-full' }, [
h('input', {
type: 'checkbox',
checked,
class: 'cursor-pointer',
onChange: onToggle
})
])
}
}
})
const ProcessHeaderRenderer = defineComponent({
name: 'ProcessHeaderRenderer',
props: {
params: {
type: Object as PropType<IHeaderParams>,
required: true
}
},
setup(props) {
const tooltipText = '默认为编制,勾选为审核'
props.params.setTooltip?.(tooltipText, () => true)
return () =>
h('div', { class: 'flex items-center justify-center gap-1 w-full' }, [
h('span', props.params.displayName || '工作环节'),
h('span', { class: 'inline-flex items-center pointer-events-auto' }, [
h(CircleHelp, {
size: 20,
class: 'text-muted-foreground pointer-events-none'
})
])
])
}
})
const columnDefs: ColDef<DetailRow>[] = [ const columnDefs: ColDef<DetailRow>[] = [
{ {
headerName: '编码', headerName: '编码',
@ -557,6 +621,29 @@ const columnDefs: ColDef<DetailRow>[] = [
return params.data.name return params.data.name
} }
}, },
{
headerName: '工作环节',
field: 'process',
headerClass: 'ag-center-header',
minWidth: 90,
maxWidth: 110,
flex: 1,
editable: false,
sortable: false,
filter: false,
cellStyle: {
textAlign: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
},
valueGetter: params => {
if (!params.data || isFixedRow(params.data)) return null
return params.data.process === 1 ? 1 : 0
},
headerComponent: ProcessHeaderRenderer,
cellRenderer: ProcessCellRenderer
},
{ {
headerName: '投资规模法', headerName: '投资规模法',
field: 'investScale', field: 'investScale',
@ -639,12 +726,12 @@ const columnDefs: ColDef<DetailRow>[] = [
valueGetter: params => { valueGetter: params => {
if (!params.data) return null if (!params.data) return null
if (isFixedRow(params.data)) return getFixedRowSubtotal() if (isFixedRow(params.data)) return getFixedRowSubtotal()
return addNumbers( return sumNullableNumbers([
valueOrZero(params.data.investScale), params.data.investScale,
valueOrZero(params.data.landScale), params.data.landScale,
valueOrZero(params.data.workload), params.data.workload,
valueOrZero(params.data.hourly) params.data.hourly
) ])
}, },
valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3)) valueFormatter: params => (params.value == null ? '' : formatThousandsFlexible(params.value, 3))
}, },
@ -666,6 +753,25 @@ const detailGridOptions: GridOptions<DetailRow> = {
...gridOptions, ...gridOptions,
treeData: false, treeData: false,
getDataPath: undefined, getDataPath: undefined,
context: {
onToggleProcess: async (rowId: string, checked: boolean) => {
const currentState = getCurrentContractState()
let changed = false
const nextRows = currentState.detailRows.map(row => {
if (isFixedRow(row) || String(row.id) !== String(rowId)) return row
changed = true
return {
...row,
process: checked ? 1 : 0
}
})
if (!changed) return
await setCurrentContractState({
...currentState,
detailRows: nextRows
})
}
},
onCellClicked: async params => { onCellClicked: async params => {
if (params.colDef.field !== 'actions' || !params.data || isFixedRow(params.data)) return if (params.colDef.field !== 'actions' || !params.data || isFixedRow(params.data)) return
const target = params.event?.target as HTMLElement | null const target = params.event?.target as HTMLElement | null
@ -698,7 +804,7 @@ const applyFixedRowTotals = (rows: DetailRow[]) => {
landScale: nextLandScale, landScale: nextLandScale,
workload: nextWorkload, workload: nextWorkload,
hourly: nextHourly, hourly: nextHourly,
subtotal: addNumbers(nextInvestScale, nextLandScale, nextWorkload, nextHourly) subtotal: sumNullableNumbers([nextInvestScale, nextLandScale, nextWorkload, nextHourly])
} }
: row : row
) )
@ -779,7 +885,7 @@ const applySelection = async (codes: string[]) => {
const existingMap = new Map(currentState.detailRows.map(row => [row.id, row])) const existingMap = new Map(currentState.detailRows.map(row => [row.id, row]))
const baseRows: DetailRow[] = uniqueIds const baseRows: DetailRow[] = uniqueIds
.map(id => { .map<DetailRow | null>(id => {
const dictItem = serviceById.value.get(id) const dictItem = serviceById.value.get(id)
if (!dictItem) return null if (!dictItem) return null
@ -794,13 +900,14 @@ const applySelection = async (codes: string[]) => {
id: old?.id || id, id: old?.id || id,
code: dictItem.code, code: dictItem.code,
name: dictItem.name, name: dictItem.name,
process: old?.process === 1 ? 1 : 0,
investScale: nextValues.investScale, investScale: nextValues.investScale,
landScale: nextValues.landScale, landScale: nextValues.landScale,
workload: nextValues.workload, workload: nextValues.workload,
hourly: nextValues.hourly hourly: nextValues.hourly
} }
}) })
.filter((row): row is DetailRow => Boolean(row)) .filter((row): row is DetailRow => row !== null)
const orderMap = new Map(serviceDict.value.map((item, index) => [item.id, index])) const orderMap = new Map(serviceDict.value.map((item, index) => [item.id, index]))
baseRows.sort((a, b) => (orderMap.get(a.id) || 0) - (orderMap.get(b.id) || 0)) baseRows.sort((a, b) => (orderMap.get(a.id) || 0) - (orderMap.get(b.id) || 0))
@ -810,6 +917,7 @@ const applySelection = async (codes: string[]) => {
id: fixedOld?.id || fixedBudgetRow.id, id: fixedOld?.id || fixedBudgetRow.id,
code: fixedBudgetRow.code, code: fixedBudgetRow.code,
name: fixedBudgetRow.name, name: fixedBudgetRow.name,
process: null,
investScale: typeof fixedOld?.investScale === 'number' ? fixedOld.investScale : null, investScale: typeof fixedOld?.investScale === 'number' ? fixedOld.investScale : null,
landScale: typeof fixedOld?.landScale === 'number' ? fixedOld.landScale : null, landScale: typeof fixedOld?.landScale === 'number' ? fixedOld.landScale : null,
workload: typeof fixedOld?.workload === 'number' ? fixedOld.workload : null, workload: typeof fixedOld?.workload === 'number' ? fixedOld.workload : null,
@ -983,6 +1091,7 @@ const initializeContractState = async () => {
id: fixedBudgetRow.id, id: fixedBudgetRow.id,
code: fixedBudgetRow.code, code: fixedBudgetRow.code,
name: fixedBudgetRow.name, name: fixedBudgetRow.name,
process: null,
investScale: null, investScale: null,
landScale: null, landScale: null,
workload: null, workload: null,

View File

@ -22,7 +22,8 @@ import {
AlertDialogTrigger, AlertDialogTrigger,
} from 'reka-ui' } from 'reka-ui'
import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive' import { decodeZwArchive, encodeZwArchive, ZW_FILE_EXTENSION } from '@/lib/zwArchive'
import { exportFile, serviceList } from '@/sql' import { addNumbers, roundTo } from '@/lib/decimal'
import { exportFile, serviceList, additionalWorkList } from '@/sql'
interface DataEntry { interface DataEntry {
key: string key: string
@ -59,12 +60,14 @@ type XmInfoLike = {
interface ScaleRowLike { interface ScaleRowLike {
id: string id: string
amount?: unknown amount: number | null
landArea?: unknown landArea: number | null
} }
interface XmInfoStorageLike extends XmInfoLike { interface XmInfoStorageLike extends XmInfoLike {
detailRows?: ScaleRowLike[] detailRows?: ScaleRowLike[],
totalAmount?: number,
roughCalcEnabled?: boolean
} }
interface XmScaleStorageLike { interface XmScaleStorageLike {
detailRows?: ScaleRowLike[] detailRows?: ScaleRowLike[]
@ -78,6 +81,7 @@ interface ContractCardItem {
interface ZxFwRowLike { interface ZxFwRowLike {
id: string id: string
process?: unknown
subtotal?: unknown subtotal?: unknown
investScale?: unknown investScale?: unknown
landScale?: unknown landScale?: unknown
@ -86,6 +90,8 @@ interface ZxFwRowLike {
} }
interface ZxFwStorageLike { interface ZxFwStorageLike {
selectedIds?: string[]
selectedCodes?: string[]
detailRows?: ZxFwRowLike[] detailRows?: ZxFwRowLike[]
} }
@ -97,6 +103,28 @@ interface ScaleMethodRowLike extends ScaleRowLike {
budgetFeeOptional?: unknown budgetFeeOptional?: unknown
consultCategoryFactor?: unknown consultCategoryFactor?: unknown
majorFactor?: unknown majorFactor?: unknown
workStageFactor?: unknown
workRatio?: unknown
remark?: unknown
}
interface HtFeeMainRowLike {
id?: unknown
name?: unknown
}
interface RateMethodRowLike {
rate?: unknown
budgetFee?: unknown
}
interface QuantityMethodRowLike {
id?: unknown
feeItem?: unknown
unit?: unknown
quantity?: unknown
unitPrice?: unknown
budgetFee?: unknown
remark?: unknown remark?: unknown
} }
@ -120,7 +148,11 @@ interface HourlyMethodRowLike {
} }
interface DetailRowsStorageLike<T> { interface DetailRowsStorageLike<T> {
detailRows?: T[] detailRows?: T[],
roughCalcEnabled?: boolean,
totalAmount?: number,
} }
interface FactorRowLike { interface FactorRowLike {
@ -132,8 +164,8 @@ interface FactorRowLike {
interface ExportScaleRow { interface ExportScaleRow {
major: number major: number
cost: number cost: number | null
area: number area: number | null
} }
interface ExportMethod1Detail { interface ExportMethod1Detail {
@ -250,8 +282,63 @@ interface ExportContract {
serviceCoes: ExportServiceCoe[] serviceCoes: ExportServiceCoe[]
majorCoes: ExportMajorCoe[] majorCoes: ExportMajorCoe[]
services: ExportService[] services: ExportService[]
addtional: [] addtional: ExportAdditional | null
reserve: [] reserve: ExportReserve | null
}
interface ExportRichTextCode {
richText: Array<{
text: string
font?: Record<string, unknown>
}>
}
interface ExportMethod0 {
coe: number
fee: number
}
interface ExportMethod5Detail {
name: string
unit: string
amount: number
price: number
fee: number
remark: string
}
interface ExportMethod5 {
fee: number
det: ExportMethod5Detail[]
}
interface ExportAdditionalDetail {
id: number
code: ExportRichTextCode
ref?: ExportRichTextCode
name: string
fee: number
m0?: ExportMethod0
m4?: ExportMethod4
m5?: ExportMethod5
}
interface ExportAdditional {
code: ExportRichTextCode
ref?: ExportRichTextCode
name: string
fee: number
det: ExportAdditionalDetail[]
}
interface ExportReserve {
code: ExportRichTextCode
ref?: ExportRichTextCode
name: string
fee: number
m0?: ExportMethod0
m4?: ExportMethod4
m5?: ExportMethod5
} }
interface ExportReportPayload { interface ExportReportPayload {
@ -872,6 +959,15 @@ const getExpertIdFromRowId = (value: string): number | null => {
const hasServiceId = (serviceId: string) => const hasServiceId = (serviceId: string) =>
Object.prototype.hasOwnProperty.call(serviceList as Record<string, unknown>, serviceId) Object.prototype.hasOwnProperty.call(serviceList as Record<string, unknown>, serviceId)
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
})
const mapIndustryCodeToExportIndustry = (value: unknown): number => { const mapIndustryCodeToExportIndustry = (value: unknown): number => {
const raw = typeof value === 'string' ? value.trim().toUpperCase() : '' const raw = typeof value === 'string' ? value.trim().toUpperCase() : ''
if (!raw) return 0 if (!raw) return 0
@ -886,12 +982,12 @@ const buildProjectServiceCoes = (rows: FactorRowLike[] | undefined): ExportServi
return rows return rows
.map(row => { .map(row => {
const serviceid = toSafeInteger(row.id) const serviceid = toSafeInteger(row.id)
if (serviceid == null) return null if (serviceid == null || row.budgetValue == null) return null
const coe = toFiniteNumber(row.budgetValue) ?? toFiniteNumber(row.standardFactor) ?? 0 const coe = toFiniteNumber(row.budgetValue)
return { return {
serviceid, serviceid,
coe, coe,
remark: typeof row.remark === 'string' ? row.remark : '' remark: row.remark
} }
}) })
.filter((item): item is ExportServiceCoe => Boolean(item)) .filter((item): item is ExportServiceCoe => Boolean(item))
@ -902,42 +998,44 @@ const buildProjectMajorCoes = (rows: FactorRowLike[] | undefined): ExportMajorCo
return rows return rows
.map(row => { .map(row => {
const majorid = toSafeInteger(row.id) const majorid = toSafeInteger(row.id)
if (majorid == null) return null if (majorid == null || row.budgetValue == null) return null
const coe = toFiniteNumber(row.budgetValue) ?? toFiniteNumber(row.standardFactor) ?? 0 const coe = toFiniteNumber(row.budgetValue)
return { return {
majorid, majorid,
coe, coe,
remark: typeof row.remark === 'string' ? row.remark : '' remark: row.remark
} }
}) })
.filter((item): item is ExportMajorCoe => Boolean(item)) .filter((item): item is ExportMajorCoe => Boolean(item))
} }
const buildScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] => { const toExportScaleRows = (rows: ScaleRowLike[] | undefined): ExportScaleRow[] => {
if (!Array.isArray(rows)) return [] if (!Array.isArray(rows)) return []
return rows return rows
.map(row => { .map(row => {
const major = toSafeInteger(row.id)
const cost = toFiniteNumber(row.amount) if (row.id == null || (row.amount == null && row.landArea == null)) return null
const area = toFiniteNumber(row.landArea)
if (major == null || (cost == null && area == null)) return null
return { return {
major, major: toSafeInteger(row.id),
cost: cost ?? 0, cost: row.amount,
area: area ?? 0 area: row.landArea
} }
}) })
.filter((item): item is ExportScaleRow => Boolean(item)) .filter((item): item is ExportScaleRow => Boolean(item))
} }
const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => { const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | null => {
if (!Array.isArray(rows)) return null if (!Array.isArray(rows)) return null
let hasTotalValue = false
const det = rows const det = rows
.map(row => { .map(row => {
const major = toSafeInteger(row.id) const major = toSafeInteger(row.id)
if (major == null) return null if (major == null || row.budgetFee ==null) return null
const cost = toFiniteNumber(row.amount) const cost = toFiniteNumber(row.amount)
const basicFee = toFiniteNumber(row.budgetFee) const basicFee = toFiniteNumber(row.budgetFee)
if (basicFee != null) hasTotalValue = true
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic) const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic)
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional) const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional)
const remark = typeof row.remark === 'string' ? row.remark : '' const remark = typeof row.remark === 'string' ? row.remark : ''
@ -958,15 +1056,16 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
basicFee_optional: basicFeeOptional ?? 0, basicFee_optional: basicFeeOptional ?? 0,
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor), serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
majorCoe: toFiniteNumberOrZero(row.majorFactor), majorCoe: toFiniteNumberOrZero(row.majorFactor),
processCoe: 1, processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
proportion: 1, proportion: toFiniteNumber(row.workRatio) ?? 1,
fee: basicFee ?? 0, fee: basicFee ?? 0,
remark remark
} }
}) })
.filter((item): item is ExportMethod1Detail => Boolean(item)) .filter((item): item is ExportMethod1Detail => Boolean(item))
if (det.length === 0) return null if (det.length === 0 || !hasTotalValue) return null
console.log(det)
return { return {
cost: sumNumbers(det.map(item => item.cost)), cost: sumNumbers(det.map(item => item.cost)),
basicFee: sumNumbers(det.map(item => item.basicFee)), basicFee: sumNumbers(det.map(item => item.basicFee)),
@ -979,12 +1078,14 @@ const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod1 | n
const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | null => { const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | null => {
if (!Array.isArray(rows)) return null if (!Array.isArray(rows)) return null
let hasTotalValue = false
const det = rows const det = rows
.map(row => { .map(row => {
const major = toSafeInteger(row.id) const major = toSafeInteger(row.id)
if (major == null) return null if (major == null || row.budgetFee ==null) return null
const area = toFiniteNumber(row.landArea) const area = toFiniteNumber(row.landArea)
const basicFee = toFiniteNumber(row.budgetFee) const basicFee = toFiniteNumber(row.budgetFee)
if (basicFee != null) hasTotalValue = true
const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic) const basicFeeBasic = toFiniteNumber(row.budgetFeeBasic)
const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional) const basicFeeOptional = toFiniteNumber(row.budgetFeeOptional)
const remark = typeof row.remark === 'string' ? row.remark : '' const remark = typeof row.remark === 'string' ? row.remark : ''
@ -1005,15 +1106,15 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
basicFee_optional: basicFeeOptional ?? 0, basicFee_optional: basicFeeOptional ?? 0,
serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor), serviceCoe: toFiniteNumberOrZero(row.consultCategoryFactor),
majorCoe: toFiniteNumberOrZero(row.majorFactor), majorCoe: toFiniteNumberOrZero(row.majorFactor),
processCoe: 1, processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
proportion: 1, proportion: toFiniteNumber(row.workRatio) ?? 1,
fee: basicFee ?? 0, fee: basicFee ?? 0,
remark remark
} }
}) })
.filter((item): item is ExportMethod2Detail => Boolean(item)) .filter((item): item is ExportMethod2Detail => Boolean(item))
if (det.length === 0) return null if (det.length === 0 || !hasTotalValue) return null
return { return {
area: sumNumbers(det.map(item => item.area)), area: sumNumbers(det.map(item => item.area)),
basicFee: sumNumbers(det.map(item => item.basicFee)), basicFee: sumNumbers(det.map(item => item.basicFee)),
@ -1026,6 +1127,7 @@ const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined): ExportMethod2 | n
const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3 | null => { const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3 | null => {
if (!Array.isArray(rows)) return null if (!Array.isArray(rows)) return null
let hasTotalValue = false
const det = rows const det = rows
.map(row => { .map(row => {
const task = getTaskIdFromRowId(row.id) const task = getTaskIdFromRowId(row.id)
@ -1033,6 +1135,7 @@ const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3
const amount = toFiniteNumber(row.workload) const amount = toFiniteNumber(row.workload)
const basicFee = toFiniteNumber(row.basicFee) const basicFee = toFiniteNumber(row.basicFee)
const fee = toFiniteNumber(row.serviceFee) const fee = toFiniteNumber(row.serviceFee)
if (fee != null) hasTotalValue = true
const remark = typeof row.remark === 'string' ? row.remark : '' const remark = typeof row.remark === 'string' ? row.remark : ''
const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark) const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark)
if (!hasValue) return null if (!hasValue) return null
@ -1048,7 +1151,7 @@ const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3
}) })
.filter((item): item is ExportMethod3Detail => Boolean(item)) .filter((item): item is ExportMethod3Detail => Boolean(item))
if (det.length === 0) return null if (det.length === 0 || !hasTotalValue) return null
return { return {
basicFee: sumNumbers(det.map(item => item.basicFee)), basicFee: sumNumbers(det.map(item => item.basicFee)),
fee: sumNumbers(det.map(item => item.fee)), fee: sumNumbers(det.map(item => item.fee)),
@ -1058,6 +1161,7 @@ const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined): ExportMethod3
const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 | null => { const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 | null => {
if (!Array.isArray(rows)) return null if (!Array.isArray(rows)) return null
let hasTotalValue = false
const det = rows const det = rows
.map(row => { .map(row => {
const expert = getExpertIdFromRowId(row.id) const expert = getExpertIdFromRowId(row.id)
@ -1065,6 +1169,7 @@ const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 |
const personNum = toFiniteNumber(row.personnelCount) const personNum = toFiniteNumber(row.personnelCount)
const workDay = toFiniteNumber(row.workdayCount) const workDay = toFiniteNumber(row.workdayCount)
const fee = toFiniteNumber(row.serviceBudget) const fee = toFiniteNumber(row.serviceBudget)
if (fee != null) hasTotalValue = true
const remark = typeof row.remark === 'string' ? row.remark : '' const remark = typeof row.remark === 'string' ? row.remark : ''
const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark) const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark)
if (!hasValue) return null if (!hasValue) return null
@ -1079,7 +1184,7 @@ const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 |
}) })
.filter((item): item is ExportMethod4Detail => Boolean(item)) .filter((item): item is ExportMethod4Detail => Boolean(item))
if (det.length === 0) return null if (det.length === 0 || !hasTotalValue) return null
return { return {
person_num: sumNumbers(det.map(item => item.person_num)), person_num: sumNumbers(det.map(item => item.person_num)),
work_day: sumNumbers(det.map(item => item.work_day)), work_day: sumNumbers(det.map(item => item.work_day)),
@ -1089,26 +1194,182 @@ const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined): ExportMethod4 |
} }
const buildServiceFee = ( const buildServiceFee = (
row: ZxFwRowLike, row: ZxFwRowLike | null | undefined,
method1: ExportMethod1 | null, method1: ExportMethod1 | null,
method2: ExportMethod2 | null, method2: ExportMethod2 | null,
method3: ExportMethod3 | null, method3: ExportMethod3 | null,
method4: ExportMethod4 | null method4: ExportMethod4 | null
) => { ) => {
const subtotal = toFiniteNumber(row.subtotal) const subtotal = toFiniteNumber(row?.subtotal)
if (subtotal != null) return subtotal if (subtotal != null) return subtotal
const methodSum = sumNumbers([method1?.fee, method2?.fee, method3?.fee, method4?.fee]) const methodSum = sumNumbers([method1?.fee, method2?.fee, method3?.fee, method4?.fee])
if (methodSum !== 0) return methodSum if (methodSum !== 0) return methodSum
return sumNumbers([ return sumNumbers([
toFiniteNumber(row.investScale), toFiniteNumber(row?.investScale),
toFiniteNumber(row.landScale), toFiniteNumber(row?.landScale),
toFiniteNumber(row.workload), toFiniteNumber(row?.workload),
toFiniteNumber(row.hourly) toFiniteNumber(row?.hourly)
]) ])
} }
const createRichTextCode = (...parts: string[]): ExportRichTextCode => ({
richText: parts
.map(item => String(item || '').trim())
.filter(Boolean)
.map(text => ({ text }))
})
const buildMethod0 = (payload: RateMethodRowLike | null | undefined): ExportMethod0 | null => {
const coe = toFiniteNumber(payload?.rate)
const fee = toFiniteNumber(payload?.budgetFee)
if (fee == null) return null
return {
coe: coe ?? 0,
fee
}
}
const buildMethod5 = (rows: QuantityMethodRowLike[] | undefined): ExportMethod5 | null => {
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 : ''
const hasValue =
quantity != null ||
unitPrice != null ||
fee != null ||
isNonEmptyString(name) ||
isNonEmptyString(unit) ||
isNonEmptyString(remark)
if (!hasValue) return null
return {
name,
unit,
amount: quantity ?? 0,
price: unitPrice ?? 0,
fee: fee ?? 0,
remark
}
})
.filter((item): item is ExportMethod5Detail => Boolean(item))
if (det.length === 0) return null
return {
fee: subtotalFee,
det
}
}
const normalizeHtFeeMainRows = (rows: HtFeeMainRowLike[] | undefined) => {
if (!Array.isArray(rows)) return [] as Array<{ id: string; name: string }>
return rows
.map(row => {
const id = String(row?.id || '').trim()
if (!id) return null
return {
id,
name: typeof row?.name === 'string' ? row.name : ''
}
})
.filter((item): item is { id: string; name: string } => Boolean(item))
}
const loadHtFeeMethodsByRow = async (mainStorageKey: string, rowId: string) => {
const [rateState, hourlyState, quantityState] = await Promise.all([
zxFwPricingStore.loadHtFeeMethodState<RateMethodRowLike>(mainStorageKey, rowId, 'rate-fee'),
zxFwPricingStore.loadHtFeeMethodState<DetailRowsStorageLike<HourlyMethodRowLike>>(mainStorageKey, rowId, 'hourly-fee'),
zxFwPricingStore.loadHtFeeMethodState<DetailRowsStorageLike<QuantityMethodRowLike>>(mainStorageKey, rowId, 'quantity-unit-price-fee')
])
const m0 = buildMethod0(rateState)
const m4 = buildMethod4(Array.isArray(hourlyState?.detailRows) ? hourlyState?.detailRows : undefined)
const m5 = buildMethod5(Array.isArray(quantityState?.detailRows) ? quantityState?.detailRows : undefined)
if (!m0 && !m4 && !m5) return null
return {
fee: sumNumbers([m0?.fee, m4?.fee, m5?.fee]),
m0,
m4,
m5
}
}
const buildAdditionalDetailCode = (index: number, name: string): ExportRichTextCode => {
const dictionaryIndex = additionalWorkList.findIndex(item => item === name)
const useIndex = dictionaryIndex >= 0 ? dictionaryIndex : index
const suffix = useIndex === 0 ? 'F' : useIndex === 1 ? 'X' : String(useIndex + 1)
return createRichTextCode('C', suffix)
}
const buildAdditionalExport = async (contractId: string): Promise<ExportAdditional | null> => {
const storageKey = `htExtraFee-${contractId}-additional-work`
const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(storageKey)
const rows = normalizeHtFeeMainRows(mainState?.detailRows)
if (rows.length === 0) return null
const det = (
await Promise.all(
rows.map(async (row, index) => {
const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
if (!methodPayload) return null
const item: ExportAdditionalDetail = {
id: index,
code: buildAdditionalDetailCode(index, row.name),
name: row.name || `附加工作-${index + 1}`,
fee: methodPayload.fee
}
item.ref = item.code
if (methodPayload.m0) item.m0 = methodPayload.m0
if (methodPayload.m4) item.m4 = methodPayload.m4
if (methodPayload.m5) item.m5 = methodPayload.m5
return item
})
)
).filter((item): item is ExportAdditionalDetail => Boolean(item))
if (det.length === 0) return null
const addtionalCode = createRichTextCode('C', 'C')
return {
code: addtionalCode,
ref: addtionalCode,
name: '附加工作',
fee: sumNumbers(det.map(item => item.fee)),
det
}
}
const buildReserveExport = async (contractId: string): Promise<ExportReserve | null> => {
const storageKey = `htExtraFee-${contractId}-reserve`
const mainState = await zxFwPricingStore.loadHtFeeMainState<HtFeeMainRowLike>(storageKey)
const rows = normalizeHtFeeMainRows(mainState?.detailRows)
if (rows.length === 0) return null
for (const row of rows) {
const methodPayload = await loadHtFeeMethodsByRow(storageKey, row.id)
if (!methodPayload) continue
const reserve: ExportReserve = {
code: createRichTextCode('Y', 'B'),
name: row.name || '预备费',
fee: methodPayload.fee
}
reserve.ref = reserve.code
if (methodPayload.m0) reserve.m0 = methodPayload.m0
if (methodPayload.m4) reserve.m4 = methodPayload.m4
if (methodPayload.m5) reserve.m5 = methodPayload.m5
return reserve
}
return null
}
const buildExportReportPayload = async (): Promise<ExportReportPayload> => { const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw] = await Promise.all([ const [projectInfoRaw, projectScaleRaw, consultCategoryFactorRaw, majorFactorRaw, contractCardsRaw] = await Promise.all([
kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY), kvStore.getItem<XmInfoLike>(PROJECT_INFO_DB_KEY),
@ -1120,8 +1381,14 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const projectInfo = projectInfoRaw || {} const projectInfo = projectInfoRaw || {}
const projectScaleSource = projectScaleRaw || {} const projectScaleSource = projectScaleRaw || {}
const projectScale = buildScaleRows(projectScaleSource.detailRows)
const projectScaleCost = sumNumbers(projectScale.map(item => item.cost)) const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows)
const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumNumbers(projectScale.map(item => item.cost))
projectScale.push({
major: -1, cost: projectScaleCost,
area: null
})
const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorRaw?.detailRows) const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorRaw?.detailRows)
const projectMajorCoes = buildProjectMajorCoes(majorFactorRaw?.detailRows) const projectMajorCoes = buildProjectMajorCoes(majorFactorRaw?.detailRows)
const projectName = isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目' const projectName = isNonEmptyString(projectInfo.projectName) ? projectInfo.projectName.trim() : '造价项目'
@ -1139,21 +1406,59 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
for (let index = 0; index < contractCards.length; index++) { for (let index = 0; index < contractCards.length; index++) {
const contract = contractCards[index] const contract = contractCards[index]
const contractId = contract.id const contractId = contract.id
const [htInfoRaw, zxFwRaw] = await Promise.all([ await zxFwPricingStore.loadContract(contractId)
const [htInfoRaw, zxFwRaw, htConsultCategoryFactorRaw, htMajorFactorRaw] = await Promise.all([
kvStore.getItem<DetailRowsStorageLike<ScaleRowLike>>(`ht-info-v3-${contractId}`), kvStore.getItem<DetailRowsStorageLike<ScaleRowLike>>(`ht-info-v3-${contractId}`),
kvStore.getItem<ZxFwStorageLike>(`zxFW-${contractId}`) kvStore.getItem<ZxFwStorageLike>(`zxFW-${contractId}`),
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(`ht-consult-category-factor-v1-${contractId}`),
kvStore.getItem<DetailRowsStorageLike<FactorRowLike>>(`ht-major-factor-v1-${contractId}`)
]) ])
const zxRows = Array.isArray(zxFwRaw?.detailRows) ? zxFwRaw.detailRows : [] const contractState = zxFwPricingStore.getContractState(contractId)
const fixedRow = zxRows.find(row => row.id === 'fixed-budget-c') const zxRowsFromStore: ZxFwRowLike[] = Array.isArray(contractState?.detailRows)
const serviceRows = zxRows.filter(row => row.id !== 'fixed-budget-c' && hasServiceId(String(row.id))) ? contractState.detailRows.map(row => ({
id: String(row.id || ''),
process: row.process,
subtotal: row.subtotal,
investScale: row.investScale,
landScale: row.landScale,
workload: row.workload,
hourly: row.hourly
}))
: []
const zxRowsFromKv = Array.isArray(zxFwRaw?.detailRows) ? zxFwRaw.detailRows : []
const zxRows = zxRowsFromStore.length > 0 ? zxRowsFromStore : zxRowsFromKv
const selectedIdsFromStore = Array.isArray(contractState?.selectedIds)
? contractState.selectedIds.map(id => String(id || '').trim()).filter(Boolean)
: []
const selectedIdsFromKv = Array.isArray(zxFwRaw?.selectedIds)
? zxFwRaw.selectedIds.map(id => String(id || '').trim()).filter(Boolean)
: []
const selectedIds = Array.from(new Set([...selectedIdsFromStore, ...selectedIdsFromKv])).filter(hasServiceId)
let fixedRow: ZxFwRowLike | undefined
const serviceRowMap = new Map<string, ZxFwRowLike>()
for (const row of zxRows) {
const rowId = String(row?.id || '').trim()
if (!rowId) continue
if (rowId === 'fixed-budget-c') {
fixedRow = row
continue
}
if (!hasServiceId(rowId)) continue
serviceRowMap.set(rowId, row)
}
const fallbackServiceIds = Array.from(serviceRowMap.keys())
const serviceIdTexts = sortServiceIdsByDict(
(selectedIds.length > 0 ? selectedIds : fallbackServiceIds).filter(hasServiceId)
)
const services = ( const services = (
await Promise.all( await Promise.all(
serviceRows.map(async row => { serviceIdTexts.map(async serviceIdText => {
const serviceIdText = String(row.id)
const serviceId = toSafeInteger(serviceIdText) const serviceId = toSafeInteger(serviceIdText)
if (serviceId == null) return null if (serviceId == null) return null
const sourceRow = serviceRowMap.get(serviceIdText)
const [method1State, method2State, method3State, method4State] = await Promise.all([ const [method1State, method2State, method3State, method4State] = await Promise.all([
zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'investScale'), zxFwPricingStore.loadServicePricingMethodState<ScaleMethodRowLike>(contractId, serviceIdText, 'investScale'),
@ -1170,11 +1475,11 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const method2 = buildMethod2(method2Raw?.detailRows) const method2 = buildMethod2(method2Raw?.detailRows)
const method3 = buildMethod3(method3Raw?.detailRows) const method3 = buildMethod3(method3Raw?.detailRows)
const method4 = buildMethod4(method4Raw?.detailRows) const method4 = buildMethod4(method4Raw?.detailRows)
const fee = buildServiceFee(row, method1, method2, method3, method4) const fee = buildServiceFee(sourceRow, method1, method2, method3, method4)
const process = Number(sourceRow?.process) === 1 ? 1 : 0
const service: ExportService = { const service: ExportService = {
id: serviceId, id: serviceId,
process: 0, process,
fee fee
} }
if (method1) service.method1 = method1 if (method1) service.method1 = method1
@ -1195,9 +1500,21 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
toFiniteNumber(fixedRow?.hourly) toFiniteNumber(fixedRow?.hourly)
]) ])
const serviceFee = fixedSubtotal ?? (serviceFeeSum !== 0 ? serviceFeeSum : fixedMethodSum) const serviceFee = fixedSubtotal ?? (serviceFeeSum !== 0 ? serviceFeeSum : fixedMethodSum)
const addtionalFee = 0 const [addtional, reserve] = await Promise.all([
const reserveFee = 0 buildAdditionalExport(contractId),
const contractFee = serviceFee + addtionalFee + reserveFee buildReserveExport(contractId)
])
const addtionalFee = addtional ? addtional.fee : 0
const reserveFee = reserve ? reserve.fee : 0
const contractFee = roundTo(addNumbers(serviceFee, addtionalFee, reserveFee), 3)
const contractScale = htInfoRaw?.roughCalcEnabled ? [] : toExportScaleRows(htInfoRaw?.detailRows)
contractScale.push({
major: -1, cost: contractFee,
area: null
})
const contractServiceCoesRaw = buildProjectServiceCoes(htConsultCategoryFactorRaw?.detailRows)
const contractMajorCoesRaw = buildProjectMajorCoes(htMajorFactorRaw?.detailRows)
contracts.push({ contracts.push({
name: isNonEmptyString(contract.name) ? contract.name : `合同段-${index + 1}`, name: isNonEmptyString(contract.name) ? contract.name : `合同段-${index + 1}`,
@ -1205,12 +1522,12 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
addtionalFee, addtionalFee,
reserveFee, reserveFee,
fee: contractFee, fee: contractFee,
scale: buildScaleRows(htInfoRaw?.detailRows), scale: contractScale,
serviceCoes: projectServiceCoes.map(item => ({ ...item })), serviceCoes: contractServiceCoesRaw,
majorCoes: projectMajorCoes.map(item => ({ ...item })), majorCoes: contractMajorCoesRaw,
services, services,
addtional: [], addtional,
reserve: [] reserve
}) })
} }
@ -1273,11 +1590,12 @@ const exportReport = async () => {
try { try {
const now = new Date() const now = new Date()
const payload = await buildExportReportPayload() const payload = await buildExportReportPayload()
console.log(payload)
const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}` const fileName = `${sanitizeFileNamePart(payload.name)}-报表-${formatExportTimestamp(now)}`
await exportFile(fileName, payload) await exportFile(fileName, payload)
} catch (error) { } catch (error) {
console.error('export report failed:', error) console.error('export report failed:', error)
window.alert('导出报表失败,请重试。') // window.alert('')
} finally { } finally {
dataMenuOpen.value = false dataMenuOpen.value = false
} }

View File

@ -32,8 +32,24 @@ export const gridOptions: GridOptions = {
groupDefaultExpanded: -1, groupDefaultExpanded: -1,
suppressFieldDotNotation: true, suppressFieldDotNotation: true,
// rowData 更新后通过稳定 ID 维持展开状态和编辑上下文。 // rowData 更新后通过稳定 ID 维持展开状态和编辑上下文。
getRowId: params => String(params.data?.id ?? params.data?.path?.join('/') ?? ''), getRowId: params => {
getDataPath: data => data.path, const id = params.data?.id
if (id != null && String(id).trim()) return String(id)
const path = Array.isArray(params.data?.path)
? params.data.path.map((segment: unknown) => String(segment ?? '').trim()).filter(Boolean)
: []
if (path.length > 0) return path.join('/')
return '__row__'
},
// 兜底避免 AG Grid #185treeData 模式下 path 不能为空数组。
getDataPath: data => {
const path = Array.isArray(data?.path)
? data.path.map((segment: unknown) => String(segment ?? '').trim()).filter(Boolean)
: []
if (path.length > 0) return path
const fallback = String(data?.id ?? '').trim()
return [fallback || '__row__']
},
getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'], getContextMenuItems: () => ['copy', 'paste', 'separator', 'export'],
defaultColDef: { defaultColDef: {
resizable: true, resizable: true,

View File

@ -25,6 +25,17 @@ interface StoredFactorState {
type MaybeNumber = number | null | undefined type MaybeNumber = number | null | undefined
const sumByNumberNullable = <T>(list: T[], pick: (item: T) => MaybeNumber): number | null => {
let hasValid = false
const total = sumByNumber(list, item => {
const value = toFiniteNumberOrNull(pick(item))
if (value == null) return null
hasValid = true
return value
})
return hasValid ? total : null
}
interface ScaleRow { interface ScaleRow {
id: string id: string
amount: number | null amount: number | null
@ -153,6 +164,17 @@ const toStoredDetailRowsState = <TRow = unknown>(state: { detailRows?: TRow[] }
const hasOwn = (obj: unknown, key: string) => const hasOwn = (obj: unknown, key: string) =>
Object.prototype.hasOwnProperty.call(obj || {}, key) Object.prototype.hasOwnProperty.call(obj || {}, key)
const getRowNumberOrFallback = (
row: Record<string, unknown> | undefined,
key: string,
fallback: number | null
) => {
if (!row) return fallback
const value = toFiniteNumberOrNull(row[key])
if (value != null) return value
return hasOwn(row, key) ? null : fallback
}
const toRowMap = <TRow extends { id: string }>(rows?: TRow[]) => { const toRowMap = <TRow extends { id: string }>(rows?: TRow[]) => {
const map = new Map<string, TRow>() const map = new Map<string, TRow>()
for (const row of rows || []) { for (const row of rows || []) {
@ -363,32 +385,31 @@ const getOnlyCostScaleBudgetFee = (
return /^\d+::/.test(id) return /^\d+::/.test(id)
}) })
if (usePerRowCalculation) { if (usePerRowCalculation) {
return sumByNumber(sourceRows, row => { return sumByNumberNullable(sourceRows, row => {
const amount = toFiniteNumberOrNull(row?.amount) const amount = toFiniteNumberOrNull(row?.amount)
if (amount == null) return null if (amount == null) return null
return getScaleBudgetFee({ return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByAmount(amount), benchmarkBudget: getBenchmarkBudgetByAmount(amount),
majorFactor: toFiniteNumberOrNull(row?.majorFactor) ?? defaultMajorFactor, majorFactor: getRowNumberOrFallback(row, 'majorFactor', defaultMajorFactor),
consultCategoryFactor: toFiniteNumberOrNull(row?.consultCategoryFactor) ?? defaultConsultCategoryFactor, consultCategoryFactor: getRowNumberOrFallback(row, 'consultCategoryFactor', defaultConsultCategoryFactor),
workStageFactor: toFiniteNumberOrNull(row?.workStageFactor) ?? 1, workStageFactor: getRowNumberOrFallback(row, 'workStageFactor', 1),
workRatio: toFiniteNumberOrNull(row?.workRatio) ?? 100 workRatio: getRowNumberOrFallback(row, 'workRatio', 100)
}) })
}) })
} }
const totalAmount = sumByNumber(sourceRows, row => const totalAmount = sumByNumberNullable(sourceRows, row =>
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
) )
if (totalAmount == null) return null
const onlyRow = const onlyRow =
sourceRows.find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) || sourceRows.find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
sourceRows.find(row => hasOwn(row, 'consultCategoryFactor') || hasOwn(row, 'majorFactor')) || sourceRows.find(row => hasOwn(row, 'consultCategoryFactor') || hasOwn(row, 'majorFactor')) ||
sourceRows[0] sourceRows[0]
const consultCategoryFactor = const consultCategoryFactor = getRowNumberOrFallback(onlyRow, 'consultCategoryFactor', defaultConsultCategoryFactor)
toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ?? defaultConsultCategoryFactor const majorFactor = getRowNumberOrFallback(onlyRow, 'majorFactor', defaultMajorFactor)
const majorFactor = const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1)
toFiniteNumberOrNull(onlyRow?.majorFactor) ?? defaultMajorFactor const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 100)
const workStageFactor = toFiniteNumberOrNull(onlyRow?.workStageFactor) ?? 1
const workRatio = toFiniteNumberOrNull(onlyRow?.workRatio) ?? 100
return getScaleBudgetFee({ return getScaleBudgetFee({
benchmarkBudget: getBenchmarkBudgetByAmount(totalAmount), benchmarkBudget: getBenchmarkBudgetByAmount(totalAmount),
majorFactor, majorFactor,
@ -405,7 +426,7 @@ const buildOnlyCostScaleDetailRows = (
majorFactorMap?: Map<string, number | null>, majorFactorMap?: Map<string, number | null>,
industryId?: string | null industryId?: string | null
) => { ) => {
const totalAmount = sumByNumber(rowsFromDb || [], row => const totalAmount = sumByNumberNullable(rowsFromDb || [], row =>
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
) )
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId) const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
@ -413,17 +434,20 @@ const buildOnlyCostScaleDetailRows = (
const onlyRow = const onlyRow =
(rowsFromDb || []).find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) || (rowsFromDb || []).find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
(rowsFromDb || []).find(row => String(row?.id || '') === onlyCostRowId) (rowsFromDb || []).find(row => String(row?.id || '') === onlyCostRowId)
const consultCategoryFactor = const consultCategoryFactor = getRowNumberOrFallback(
toFiniteNumberOrNull(onlyRow?.consultCategoryFactor) ?? onlyRow,
consultCategoryFactorMap?.get(String(serviceId)) ?? 'consultCategoryFactor',
getDefaultConsultCategoryFactor(serviceId) consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
const majorFactor = )
toFiniteNumberOrNull(onlyRow?.majorFactor) ?? const majorFactor = getRowNumberOrFallback(
onlyRow,
'majorFactor',
(industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ?? (industryMajorEntry ? majorFactorMap?.get(industryMajorEntry.id) ?? null : null) ??
toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ?? toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ??
1 1
const workStageFactor = toFiniteNumberOrNull(onlyRow?.workStageFactor) ?? 1 )
const workRatio = toFiniteNumberOrNull(onlyRow?.workRatio) ?? 100 const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1)
const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 100)
return [ return [
{ {
@ -757,7 +781,7 @@ export const getPricingMethodTotalsForService = async (params: {
consultCategoryFactorMap, consultCategoryFactorMap,
majorFactorMap majorFactorMap
) )
return sumByNumber(investRows, row => { return sumByNumberNullable(investRows, row => {
if (!isCostMajorById(row.id)) return null if (!isCostMajorById(row.id)) return null
if (excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return null if (excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return null
return getInvestmentBudgetFee(row) return getInvestmentBudgetFee(row)
@ -771,13 +795,13 @@ export const getPricingMethodTotalsForService = async (params: {
consultCategoryFactorMap, consultCategoryFactorMap,
majorFactorMap majorFactorMap
) )
const landScale = sumByNumber(landRows, row => (isAreaMajorById(row.id) ? getLandBudgetFee(row) : null)) const landScale = sumByNumberNullable(landRows, row => (isAreaMajorById(row.id) ? getLandBudgetFee(row) : null))
const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap) const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId, consultCategoryFactorMap)
const workload = const workload =
defaultWorkloadRows.length === 0 defaultWorkloadRows.length === 0
? null ? null
: sumByNumber( : sumByNumberNullable(
workloadData?.detailRows != null workloadData?.detailRows != null
? mergeWorkloadRows(serviceId, workloadData.detailRows as any, consultCategoryFactorMap) ? mergeWorkloadRows(serviceId, workloadData.detailRows as any, consultCategoryFactorMap)
: defaultWorkloadRows, : defaultWorkloadRows,
@ -788,7 +812,7 @@ export const getPricingMethodTotalsForService = async (params: {
hourlyData?.detailRows != null hourlyData?.detailRows != null
? mergeHourlyRows(hourlyData.detailRows as any) ? mergeHourlyRows(hourlyData.detailRows as any)
: buildDefaultHourlyRows() : buildDefaultHourlyRows()
const hourly = sumByNumber(hourlyRows, row => calcHourlyServiceBudget(row)) const hourly = sumByNumberNullable(hourlyRows, row => calcHourlyServiceBudget(row))
return { return {
investScale, investScale,

View File

@ -11,6 +11,7 @@ export interface ZxFwDetailRow {
id: string id: string
code?: string code?: string
name?: string name?: string
process?: number | null
investScale: number | null investScale: number | null
landScale: number | null landScale: number | null
workload: number | null workload: number | null
@ -64,18 +65,31 @@ const dbKeyOf = (contractId: string) => `zxFW-${contractId}`
const serviceMethodDbKeyOf = (contractId: string, serviceId: string, method: ServicePricingMethod) => const serviceMethodDbKeyOf = (contractId: string, serviceId: string, method: ServicePricingMethod) =>
`${METHOD_STORAGE_PREFIX_MAP[method]}-${contractId}-${serviceId}` `${METHOD_STORAGE_PREFIX_MAP[method]}-${contractId}-${serviceId}`
const round3 = (value: number) => Number(value.toFixed(3)) const round3 = (value: number) => Number(value.toFixed(3))
const toNumberOrZero = (value: unknown) => { const isFiniteNumberValue = (value: unknown): value is number =>
const numeric = Number(value) typeof value === 'number' && Number.isFinite(value)
return Number.isFinite(numeric) ? numeric : 0 const sumNullableNumbers = (values: Array<number | null | undefined>): number | null => {
const validValues = values.filter(isFiniteNumberValue)
if (validValues.length === 0) return null
return addNumbers(...validValues)
}
const round3Nullable = (value: number | null | undefined) => {
const numeric = toFiniteNumberOrNull(value)
return numeric == null ? null : round3(numeric)
}
const normalizeProcessValue = (value: unknown, rowId: string) => {
if (rowId === FIXED_ROW_ID) return null
return Number(value) === 1 ? 1 : 0
} }
const normalizeRows = (rows: unknown): ZxFwDetailRow[] => const normalizeRows = (rows: unknown): ZxFwDetailRow[] =>
(Array.isArray(rows) ? rows : []).map(item => { (Array.isArray(rows) ? rows : []).map(item => {
const row = item as Partial<ZxFwDetailRow> const row = item as Partial<ZxFwDetailRow>
const rowId = String(row.id || '')
return { return {
id: String(row.id || ''), id: rowId,
code: typeof row.code === 'string' ? row.code : '', code: typeof row.code === 'string' ? row.code : '',
name: typeof row.name === 'string' ? row.name : '', name: typeof row.name === 'string' ? row.name : '',
process: normalizeProcessValue(row.process, rowId),
investScale: toFiniteNumberOrNull(row.investScale), investScale: toFiniteNumberOrNull(row.investScale),
landScale: toFiniteNumberOrNull(row.landScale), landScale: toFiniteNumberOrNull(row.landScale),
workload: toFiniteNumberOrNull(row.workload), workload: toFiniteNumberOrNull(row.workload),
@ -88,32 +102,32 @@ const normalizeRows = (rows: unknown): ZxFwDetailRow[] =>
const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => { const applyRowSubtotals = (rows: ZxFwDetailRow[]): ZxFwDetailRow[] => {
const normalized = rows.map(row => ({ ...row })) const normalized = rows.map(row => ({ ...row }))
const nonFixedRows = normalized.filter(row => row.id !== FIXED_ROW_ID) const nonFixedRows = normalized.filter(row => row.id !== FIXED_ROW_ID)
const totalInvestScale = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.investScale)), 0) const totalInvestScale = sumNullableNumbers(nonFixedRows.map(row => row.investScale))
const totalLandScale = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.landScale)), 0) const totalLandScale = sumNullableNumbers(nonFixedRows.map(row => row.landScale))
const totalWorkload = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.workload)), 0) const totalWorkload = sumNullableNumbers(nonFixedRows.map(row => row.workload))
const totalHourly = nonFixedRows.reduce((sum, row) => addNumbers(sum, toNumberOrZero(row.hourly)), 0) const totalHourly = sumNullableNumbers(nonFixedRows.map(row => row.hourly))
const fixedSubtotal = addNumbers(totalInvestScale, totalLandScale, totalWorkload, totalHourly) const fixedSubtotal = sumNullableNumbers([totalInvestScale, totalLandScale, totalWorkload, totalHourly])
return normalized.map(row => { return normalized.map(row => {
if (row.id === FIXED_ROW_ID) { if (row.id === FIXED_ROW_ID) {
return { return {
...row, ...row,
investScale: round3(totalInvestScale), investScale: round3Nullable(totalInvestScale),
landScale: round3(totalLandScale), landScale: round3Nullable(totalLandScale),
workload: round3(totalWorkload), workload: round3Nullable(totalWorkload),
hourly: round3(totalHourly), hourly: round3Nullable(totalHourly),
subtotal: round3(fixedSubtotal) subtotal: round3Nullable(fixedSubtotal)
} }
} }
const subtotal = addNumbers( const subtotal = sumNullableNumbers([
toNumberOrZero(row.investScale), row.investScale,
toNumberOrZero(row.landScale), row.landScale,
toNumberOrZero(row.workload), row.workload,
toNumberOrZero(row.hourly) row.hourly
) ])
return { return {
...row, ...row,
subtotal: round3(subtotal) subtotal: round3Nullable(subtotal)
} }
}) })
} }
@ -161,6 +175,7 @@ const isSameRows = (a: ZxFwDetailRow[] | undefined, b: ZxFwDetailRow[] | undefin
if (l.id !== r.id) return false if (l.id !== r.id) return false
if ((l.code || '') !== (r.code || '')) return false if ((l.code || '') !== (r.code || '')) return false
if ((l.name || '') !== (r.name || '')) return false if ((l.name || '') !== (r.name || '')) return false
if (normalizeProcessValue(l.process, l.id) !== normalizeProcessValue(r.process, r.id)) return false
if (!isSameNullableNumber(l.investScale, r.investScale)) return false if (!isSameNullableNumber(l.investScale, r.investScale)) return false
if (!isSameNullableNumber(l.landScale, r.landScale)) return false if (!isSameNullableNumber(l.landScale, r.landScale)) return false
if (!isSameNullableNumber(l.workload, r.workload)) return false if (!isSameNullableNumber(l.workload, r.workload)) return false
@ -895,12 +910,14 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
const fixedSubtotal = toFiniteNumberOrNull(fixedRow?.subtotal) const fixedSubtotal = toFiniteNumberOrNull(fixedRow?.subtotal)
if (fixedSubtotal != null) return round3(fixedSubtotal) if (fixedSubtotal != null) return round3(fixedSubtotal)
let hasValid = false
const sum = state.detailRows.reduce((acc, row) => { const sum = state.detailRows.reduce((acc, row) => {
if (String(row.id || '') === FIXED_ROW_ID) return acc if (String(row.id || '') === FIXED_ROW_ID) return acc
const subtotal = toFiniteNumberOrNull(row.subtotal) const subtotal = toFiniteNumberOrNull(row.subtotal)
if (subtotal != null) hasValid = true
return subtotal == null ? acc : acc + subtotal return subtotal == null ? acc : acc + subtotal
}, 0) }, 0)
return round3(sum) return hasValid ? round3(sum) : null
} }
const removeContractData = (contractIdRaw: string | number) => { const removeContractData = (contractIdRaw: string | number) => {