fix
This commit is contained in:
parent
3d26b0b259
commit
0f71fff9ac
@ -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',
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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 }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
将使用合同默认数据覆盖当前投资规模明细,是否继续?
|
将使用合同默认数据覆盖当前投资规模明细,是否继续?
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 #185:treeData 模式下 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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user