1
This commit is contained in:
parent
fd1b782803
commit
2a224c74ff
BIN
release/JGJS2026-dist.zip
Normal file
BIN
release/JGJS2026-dist.zip
Normal file
Binary file not shown.
@ -54,6 +54,7 @@ import {
|
|||||||
SERVICE_KEY_PREFIX,
|
SERVICE_KEY_PREFIX,
|
||||||
type ContractSegmentPackage
|
type ContractSegmentPackage
|
||||||
} from '@/lib/contractSegment'
|
} from '@/lib/contractSegment'
|
||||||
|
import { buildDefaultProjectScaleState } from '@/lib/projectWorkspace'
|
||||||
import { getIndustryDisplayName, industryTypeList } from '@/sql'
|
import { getIndustryDisplayName, industryTypeList } from '@/sql'
|
||||||
import { roundTo, sumNullableNumbers, toFiniteNumber } from '@/lib/decimal'
|
import { roundTo, sumNullableNumbers, toFiniteNumber } from '@/lib/decimal'
|
||||||
import { formatThousands } from '@/lib/numberFormat'
|
import { formatThousands } from '@/lib/numberFormat'
|
||||||
@ -517,15 +518,21 @@ const saveContracts = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initializeContractScaleData = async (contractId: string) => {
|
const initializeContractScaleData = async (contractId: string) => {
|
||||||
const source = await kvStore.getItem<XmScaleState>(PROJECT_SCALE_KEY)
|
const [source, projectInfo] = await Promise.all([
|
||||||
const payload: XmScaleState = {
|
kvStore.getItem<XmScaleState>(PROJECT_SCALE_KEY),
|
||||||
detailRows: Array.isArray(source?.detailRows) ? cloneJson(source.detailRows) : [],
|
kvStore.getItem<XmBaseInfoState>(PROJECT_INFO_KEY)
|
||||||
roughCalcEnabled: Boolean(source?.roughCalcEnabled),
|
])
|
||||||
totalAmount:
|
const industry = typeof projectInfo?.projectIndustry === 'string' ? projectInfo.projectIndustry.trim() : ''
|
||||||
typeof source?.totalAmount === 'number' && Number.isFinite(source.totalAmount)
|
const payload: XmScaleState = industry
|
||||||
? source.totalAmount
|
? buildDefaultProjectScaleState(industry, source)
|
||||||
: null
|
: {
|
||||||
}
|
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)
|
await kvStore.setItem(`${CONTRACT_KEY_PREFIX}${contractId}`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -250,7 +250,7 @@ const totalRow = computed<SummaryRow>(() => {
|
|||||||
landScale: sumField(row => row.landScale),
|
landScale: sumField(row => row.landScale),
|
||||||
workload: sumField(row => row.workload),
|
workload: sumField(row => row.workload),
|
||||||
hourly: sumField(row => row.hourly),
|
hourly: sumField(row => row.hourly),
|
||||||
subtotal: sumField(row => row.subtotal),
|
subtotal: null,
|
||||||
finalFee: sumField(row => row.finalFee)
|
finalFee: sumField(row => row.finalFee)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -328,7 +328,7 @@ const columnDefs: ColDef<SummaryRow>[] = [
|
|||||||
},
|
},
|
||||||
colSpan: params => {
|
colSpan: params => {
|
||||||
if (!params.data) return 1
|
if (!params.data) return 1
|
||||||
if (params.data.rowType === 'total') return 4
|
if (params.data.rowType === 'total') return 5
|
||||||
if (params.data.rowType === 'additional' || params.data.rowType === 'reserve') return 5
|
if (params.data.rowType === 'additional' || params.data.rowType === 'reserve') return 5
|
||||||
return 1
|
return 1
|
||||||
},
|
},
|
||||||
|
|||||||
@ -63,7 +63,9 @@ import { buildProjectScopedSessionKey } from '@/lib/pricingPersistControl'
|
|||||||
import {
|
import {
|
||||||
buildContractScaleIdMap,
|
buildContractScaleIdMap,
|
||||||
buildContractScaleMap,
|
buildContractScaleMap,
|
||||||
|
buildContractScaleProjectTotals,
|
||||||
getContractScaleRowByMajor,
|
getContractScaleRowByMajor,
|
||||||
|
getContractScaleProjectTotalsByRow,
|
||||||
makeProjectMajorKey,
|
makeProjectMajorKey,
|
||||||
normalizeChangedScaleRowIds,
|
normalizeChangedScaleRowIds,
|
||||||
parseProjectIndexFromPathKey,
|
parseProjectIndexFromPathKey,
|
||||||
@ -328,8 +330,15 @@ const buildDefaultRows = (projectCountValue = getTargetProjectCount()): DetailRo
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const calcOnlyCostScaleAmountFromRows = (rows?: Array<{ amount?: unknown }>) =>
|
const calcOnlyCostScaleAmountFromRows = (
|
||||||
sumByNumber(rows || [], row => (typeof row?.amount === 'number' ? row.amount : null))
|
rows?: Array<{ amount?: unknown; isGroupRow?: unknown }>,
|
||||||
|
totalAmount?: number | null
|
||||||
|
) => {
|
||||||
|
if (typeof totalAmount === 'number' && Number.isFinite(totalAmount)) return totalAmount
|
||||||
|
const summaryRow = (rows || []).find(row => row?.isGroupRow === true)
|
||||||
|
if (typeof summaryRow?.amount === 'number' && Number.isFinite(summaryRow.amount)) return summaryRow.amount
|
||||||
|
return sumByNumber(rows || [], row => (typeof row?.amount === 'number' ? row.amount : null))
|
||||||
|
}
|
||||||
|
|
||||||
const getOnlyCostScaleMajorEntry = () => {
|
const getOnlyCostScaleMajorEntry = () => {
|
||||||
const industryId = String(activeIndustryCode.value || '').trim()
|
const industryId = String(activeIndustryCode.value || '').trim()
|
||||||
@ -405,10 +414,22 @@ const buildOnlyCostScaleRow = (
|
|||||||
|
|
||||||
const buildOnlyCostScaleRows = (
|
const buildOnlyCostScaleRows = (
|
||||||
rowsFromDb?: Array<Partial<DetailRow> & Pick<DetailRow, 'id'>>,
|
rowsFromDb?: Array<Partial<DetailRow> & Pick<DetailRow, 'id'>>,
|
||||||
options?: { projectCount?: number; cloneFromProjectOne?: boolean }
|
options?: {
|
||||||
|
projectCount?: number
|
||||||
|
cloneFromProjectOne?: boolean
|
||||||
|
totalAmount?: number | null
|
||||||
|
preferSummaryAmountWhenSingleRow?: boolean
|
||||||
|
}
|
||||||
): DetailRow[] => {
|
): DetailRow[] => {
|
||||||
const targetProjectCount = normalizeProjectCount(options?.projectCount ?? getTargetProjectCount())
|
const targetProjectCount = normalizeProjectCount(options?.projectCount ?? getTargetProjectCount())
|
||||||
const onlyCostMajorId = getOnlyCostScaleMajorEntry()?.id || ONLY_COST_SCALE_ROW_ID
|
const onlyCostMajorId = getOnlyCostScaleMajorEntry()?.id || ONLY_COST_SCALE_ROW_ID
|
||||||
|
const singleRowSummaryAmount =
|
||||||
|
options?.preferSummaryAmountWhenSingleRow
|
||||||
|
? calcOnlyCostScaleAmountFromRows(
|
||||||
|
rowsFromDb as Array<{ amount?: unknown; isGroupRow?: unknown }>,
|
||||||
|
options.totalAmount
|
||||||
|
)
|
||||||
|
: null
|
||||||
const dbValueMap = new Map<string, Partial<DetailRow> & Pick<DetailRow, 'id'>>()
|
const dbValueMap = new Map<string, Partial<DetailRow> & Pick<DetailRow, 'id'>>()
|
||||||
for (const row of rowsFromDb || []) {
|
for (const row of rowsFromDb || []) {
|
||||||
const projectIndex = resolveRowProjectIndex(row)
|
const projectIndex = resolveRowProjectIndex(row)
|
||||||
@ -428,11 +449,11 @@ const buildOnlyCostScaleRows = (
|
|||||||
(options?.cloneFromProjectOne && projectIndex > 1 ? dbValueMap.get(firstProjectKey) : undefined)
|
(options?.cloneFromProjectOne && projectIndex > 1 ? dbValueMap.get(firstProjectKey) : undefined)
|
||||||
const fallbackAmount =
|
const fallbackAmount =
|
||||||
options?.cloneFromProjectOne && projectIndex > 1 && fromDb == null
|
options?.cloneFromProjectOne && projectIndex > 1 && fromDb == null
|
||||||
? calcOnlyCostScaleAmountFromRows(rowsFromDb)
|
? calcOnlyCostScaleAmountFromRows(rowsFromDb as Array<{ amount?: unknown; isGroupRow?: unknown }>, options.totalAmount)
|
||||||
: null
|
: null
|
||||||
result.push(
|
result.push(
|
||||||
buildOnlyCostScaleRow(
|
buildOnlyCostScaleRow(
|
||||||
typeof fromDb?.amount === 'number' ? fromDb.amount : fallbackAmount,
|
singleRowSummaryAmount ?? (typeof fromDb?.amount === 'number' ? fromDb.amount : fallbackAmount),
|
||||||
projectIndex,
|
projectIndex,
|
||||||
fromDb
|
fromDb
|
||||||
)
|
)
|
||||||
@ -608,22 +629,24 @@ const getBudgetFeeSplit = (
|
|||||||
) => getScaleBudgetFeeSplitByRow(row, 'cost')
|
) => getScaleBudgetFeeSplitByRow(row, 'cost')
|
||||||
|
|
||||||
const restoreAmountColumnDefaults = async () => {
|
const restoreAmountColumnDefaults = async () => {
|
||||||
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[]; totalAmount?: number | null }>(HT_DB_KEY.value)
|
||||||
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
||||||
const sourceRowMap = buildContractScaleMap(sourceRows)
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||||
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
||||||
|
const projectTotals = buildContractScaleProjectTotals(sourceRows, htData?.totalAmount)
|
||||||
|
const useSummaryAmount = detailRows.value.length === 1 || isOnlyCostScaleService.value
|
||||||
const onlyCostScaleFallbackAmount = isOnlyCostScaleService.value
|
const onlyCostScaleFallbackAmount = isOnlyCostScaleService.value
|
||||||
? calcOnlyCostScaleAmountFromRows(sourceRows as Array<{ amount?: unknown }>)
|
? calcOnlyCostScaleAmountFromRows(sourceRows as Array<{ amount?: unknown; isGroupRow?: unknown }>, htData?.totalAmount)
|
||||||
: null
|
: null
|
||||||
await restoreScaleColumnDefaults({
|
await restoreScaleColumnDefaults({
|
||||||
gridApi: gridApi.value,
|
gridApi: gridApi.value,
|
||||||
rows: detailRows.value,
|
rows: detailRows.value,
|
||||||
getCurrentValue: row => row.amount,
|
getCurrentValue: row => row.amount,
|
||||||
getNextValue: row => {
|
getNextValue: row => {
|
||||||
|
if (!row.hasCost) return null
|
||||||
|
if (useSummaryAmount) return getContractScaleProjectTotalsByRow(row, projectTotals).amount
|
||||||
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap, sourceRowIdMap)
|
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap, sourceRowIdMap)
|
||||||
return row.hasCost
|
return typeof sourceRow?.amount === 'number' ? sourceRow.amount : onlyCostScaleFallbackAmount
|
||||||
? (typeof sourceRow?.amount === 'number' ? sourceRow.amount : onlyCostScaleFallbackAmount)
|
|
||||||
: null
|
|
||||||
},
|
},
|
||||||
isSameValue: isSameNullableNumber,
|
isSameValue: isSameNullableNumber,
|
||||||
applyValue: (row, nextValue) => {
|
applyValue: (row, nextValue) => {
|
||||||
@ -812,24 +835,28 @@ const syncLinkedFieldsFromContractAndFactors = async () => {
|
|||||||
|
|
||||||
const syncLinkedScaleValuesFromContract = async (changedRowIds?: string[]) => {
|
const syncLinkedScaleValuesFromContract = async (changedRowIds?: string[]) => {
|
||||||
if (detailRows.value.length === 0) return
|
if (detailRows.value.length === 0) return
|
||||||
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[]; totalAmount?: number | null }>(HT_DB_KEY.value)
|
||||||
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
||||||
const sourceRowMap = buildContractScaleMap(sourceRows)
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||||
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
||||||
|
const projectTotals = buildContractScaleProjectTotals(sourceRows, htData?.totalAmount)
|
||||||
|
const useSummaryAmount = detailRows.value.length === 1 || isOnlyCostScaleService.value
|
||||||
const onlyCostScaleFallbackAmount = isOnlyCostScaleService.value
|
const onlyCostScaleFallbackAmount = isOnlyCostScaleService.value
|
||||||
? calcOnlyCostScaleAmountFromRows(sourceRows as Array<{ amount?: unknown }>)
|
? calcOnlyCostScaleAmountFromRows(sourceRows as Array<{ amount?: unknown; isGroupRow?: unknown }>, htData?.totalAmount)
|
||||||
: null
|
: null
|
||||||
const changedRowIdSet = changedRowIds?.length ? normalizeChangedScaleRowIds(changedRowIds) : null
|
const changedRowIdSet = changedRowIds?.length ? normalizeChangedScaleRowIds(changedRowIds) : null
|
||||||
|
|
||||||
let changed = false
|
let changed = false
|
||||||
for (const row of detailRows.value) {
|
for (const row of detailRows.value) {
|
||||||
if (changedRowIdSet) {
|
if (changedRowIdSet && !useSummaryAmount) {
|
||||||
const rowMajorId = resolveRowMajorDictId(row)
|
const rowMajorId = resolveRowMajorDictId(row)
|
||||||
if (!changedRowIdSet.has(rowMajorId)) continue
|
if (!changedRowIdSet.has(rowMajorId)) continue
|
||||||
}
|
}
|
||||||
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap, sourceRowIdMap)
|
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap, sourceRowIdMap)
|
||||||
const nextAmount = row.hasCost
|
const nextAmount = row.hasCost
|
||||||
? (typeof sourceRow?.amount === 'number' ? sourceRow.amount : onlyCostScaleFallbackAmount)
|
? (useSummaryAmount
|
||||||
|
? getContractScaleProjectTotalsByRow(row, projectTotals).amount
|
||||||
|
: (typeof sourceRow?.amount === 'number' ? sourceRow.amount : onlyCostScaleFallbackAmount))
|
||||||
: null
|
: null
|
||||||
if (isSameNullableNumber(row.amount, nextAmount)) continue
|
if (isSameNullableNumber(row.amount, nextAmount)) continue
|
||||||
row.amount = nextAmount
|
row.amount = nextAmount
|
||||||
@ -866,12 +893,21 @@ const buildRowsFromImportDefaultSource = async (
|
|||||||
): Promise<DetailRow[]> => {
|
): Promise<DetailRow[]> => {
|
||||||
// 与“使用默认数据”同源:先强制刷新系数,再按合同卡片默认带出。
|
// 与“使用默认数据”同源:先强制刷新系数,再按合同卡片默认带出。
|
||||||
await loadFactorDefaults()
|
await loadFactorDefaults()
|
||||||
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[]; totalAmount?: number | null }>(HT_DB_KEY.value)
|
||||||
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
const hasContractRows = Array.isArray(htData?.detailRows) && htData.detailRows.length > 0
|
||||||
if (isOnlyCostScaleService.value) {
|
if (isOnlyCostScaleService.value) {
|
||||||
return hasContractRows
|
return hasContractRows
|
||||||
? buildOnlyCostScaleRows(htData!.detailRows as any, { projectCount: targetProjectCount, cloneFromProjectOne: true })
|
? buildOnlyCostScaleRows(htData!.detailRows as any, {
|
||||||
: buildOnlyCostScaleRows(undefined, { projectCount: targetProjectCount })
|
projectCount: targetProjectCount,
|
||||||
|
cloneFromProjectOne: true,
|
||||||
|
totalAmount: htData?.totalAmount ?? null,
|
||||||
|
preferSummaryAmountWhenSingleRow: true
|
||||||
|
})
|
||||||
|
: buildOnlyCostScaleRows(undefined, {
|
||||||
|
projectCount: targetProjectCount,
|
||||||
|
totalAmount: htData?.totalAmount ?? null,
|
||||||
|
preferSummaryAmountWhenSingleRow: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return hasContractRows
|
return hasContractRows
|
||||||
? mergeWithDictRows(htData!.detailRows, {
|
? mergeWithDictRows(htData!.detailRows, {
|
||||||
|
|||||||
@ -63,7 +63,9 @@ import { buildProjectScopedSessionKey } from '@/lib/pricingPersistControl'
|
|||||||
import {
|
import {
|
||||||
buildContractScaleIdMap,
|
buildContractScaleIdMap,
|
||||||
buildContractScaleMap,
|
buildContractScaleMap,
|
||||||
|
buildContractScaleProjectTotals,
|
||||||
getContractScaleRowByMajor,
|
getContractScaleRowByMajor,
|
||||||
|
getContractScaleProjectTotalsByRow,
|
||||||
makeProjectMajorKey,
|
makeProjectMajorKey,
|
||||||
normalizeChangedScaleRowIds,
|
normalizeChangedScaleRowIds,
|
||||||
parseProjectIndexFromPathKey,
|
parseProjectIndexFromPathKey,
|
||||||
@ -487,17 +489,21 @@ const formatEditableFlexibleNumber = (params: any) =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
const restoreLandAreaColumnDefaults = async () => {
|
const restoreLandAreaColumnDefaults = async () => {
|
||||||
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[]; totalAmount?: number | null }>(HT_DB_KEY.value)
|
||||||
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
||||||
const sourceRowMap = buildContractScaleMap(sourceRows)
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||||
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
||||||
|
const projectTotals = buildContractScaleProjectTotals(sourceRows, htData?.totalAmount)
|
||||||
|
const useSummaryLandArea = detailRows.value.length === 1
|
||||||
await restoreScaleColumnDefaults({
|
await restoreScaleColumnDefaults({
|
||||||
gridApi: gridApi.value,
|
gridApi: gridApi.value,
|
||||||
rows: detailRows.value,
|
rows: detailRows.value,
|
||||||
getCurrentValue: row => row.landArea,
|
getCurrentValue: row => row.landArea,
|
||||||
getNextValue: row => {
|
getNextValue: row => {
|
||||||
|
if (!row.hasArea) return null
|
||||||
|
if (useSummaryLandArea) return getContractScaleProjectTotalsByRow(row, projectTotals).landArea
|
||||||
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap, sourceRowIdMap)
|
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap, sourceRowIdMap)
|
||||||
return row.hasArea ? (typeof sourceRow?.landArea === 'number' ? sourceRow.landArea : null) : null
|
return typeof sourceRow?.landArea === 'number' ? sourceRow.landArea : null
|
||||||
},
|
},
|
||||||
isSameValue: isSameNullableNumber,
|
isSameValue: isSameNullableNumber,
|
||||||
applyValue: (row, nextValue) => {
|
applyValue: (row, nextValue) => {
|
||||||
@ -690,20 +696,26 @@ const syncLinkedFieldsFromContractAndFactors = async () => {
|
|||||||
|
|
||||||
const syncLinkedScaleValuesFromContract = async (changedRowIds?: string[]) => {
|
const syncLinkedScaleValuesFromContract = async (changedRowIds?: string[]) => {
|
||||||
if (detailRows.value.length === 0) return
|
if (detailRows.value.length === 0) return
|
||||||
const htData = await kvStore.getItem<{ detailRows: SourceRow[] }>(HT_DB_KEY.value)
|
const htData = await kvStore.getItem<{ detailRows: SourceRow[]; totalAmount?: number | null }>(HT_DB_KEY.value)
|
||||||
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
||||||
const sourceRowMap = buildContractScaleMap(sourceRows)
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||||
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
||||||
|
const projectTotals = buildContractScaleProjectTotals(sourceRows, htData?.totalAmount)
|
||||||
|
const useSummaryLandArea = detailRows.value.length === 1
|
||||||
const changedRowIdSet = changedRowIds?.length ? normalizeChangedScaleRowIds(changedRowIds) : null
|
const changedRowIdSet = changedRowIds?.length ? normalizeChangedScaleRowIds(changedRowIds) : null
|
||||||
|
|
||||||
let changed = false
|
let changed = false
|
||||||
for (const row of detailRows.value) {
|
for (const row of detailRows.value) {
|
||||||
if (changedRowIdSet) {
|
if (changedRowIdSet && !useSummaryLandArea) {
|
||||||
const rowMajorId = resolveRowMajorDictId(row)
|
const rowMajorId = resolveRowMajorDictId(row)
|
||||||
if (!changedRowIdSet.has(rowMajorId)) continue
|
if (!changedRowIdSet.has(rowMajorId)) continue
|
||||||
}
|
}
|
||||||
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap, sourceRowIdMap)
|
const sourceRow = getContractScaleRowByMajor(row, sourceRowMap, sourceRowIdMap)
|
||||||
const nextLandArea = row.hasArea ? (typeof sourceRow?.landArea === 'number' ? sourceRow.landArea : null) : null
|
const nextLandArea = row.hasArea
|
||||||
|
? (useSummaryLandArea
|
||||||
|
? getContractScaleProjectTotalsByRow(row, projectTotals).landArea
|
||||||
|
: (typeof sourceRow?.landArea === 'number' ? sourceRow.landArea : null))
|
||||||
|
: null
|
||||||
if (isSameNullableNumber(row.landArea, nextLandArea)) continue
|
if (isSameNullableNumber(row.landArea, nextLandArea)) continue
|
||||||
row.landArea = nextLandArea
|
row.landArea = nextLandArea
|
||||||
changed = true
|
changed = true
|
||||||
|
|||||||
@ -77,11 +77,21 @@ const numberFormatter = (params: { value?: unknown }) =>
|
|||||||
? formatThousandsFlexible(params.value, 3)
|
? formatThousandsFlexible(params.value, 3)
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
|
const scaleValueColumnField = computed<keyof ScaleDetailRow>(() =>
|
||||||
|
isInvestmentFormula.value ? 'amount' : 'landArea'
|
||||||
|
)
|
||||||
|
|
||||||
|
const scaleValueColumnHeader = computed(() =>
|
||||||
|
isInvestmentFormula.value
|
||||||
|
? t('pricingScale.columns.investAmount')
|
||||||
|
: t('pricingScale.columns.landArea')
|
||||||
|
)
|
||||||
|
|
||||||
const columnDefs = computed<ColDef<ScaleDetailRow>[]>(() =>
|
const columnDefs = computed<ColDef<ScaleDetailRow>[]>(() =>
|
||||||
withReadonlyAutoHeight<ScaleDetailRow>([
|
withReadonlyAutoHeight<ScaleDetailRow>([
|
||||||
{
|
{
|
||||||
headerName: t('zxFwView.formulaColumns.amount'),
|
headerName: scaleValueColumnHeader.value,
|
||||||
field: 'budgetFee',
|
field: scaleValueColumnField.value,
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
@ -92,7 +102,7 @@ const columnDefs = computed<ColDef<ScaleDetailRow>[]>(() =>
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
headerName: t('pricingScale.columns.basicWork'),
|
headerName: t('pricingScale.columns.basicWork'),
|
||||||
field: 'budgetFeeBasic',
|
field: 'benchmarkBudgetBasic',
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
@ -109,7 +119,7 @@ const columnDefs = computed<ColDef<ScaleDetailRow>[]>(() =>
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
headerName: t('pricingScale.columns.optionalWork'),
|
headerName: t('pricingScale.columns.optionalWork'),
|
||||||
field: 'budgetFeeOptional',
|
field: 'benchmarkBudgetOptional',
|
||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
headerClass: 'ag-right-aligned-header',
|
headerClass: 'ag-right-aligned-header',
|
||||||
|
|||||||
@ -141,7 +141,6 @@ export interface FactorRowLike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportScaleRow {
|
export interface ExportScaleRow {
|
||||||
majorid: number
|
|
||||||
major: number
|
major: number
|
||||||
cost: number | null
|
cost: number | null
|
||||||
area: number | null
|
area: number | null
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
X
|
X
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { getIndustryDisplayName, industryTypeList } from '@/sql'
|
import { getIndustryDisplayName, industryTypeList } from '@/sql'
|
||||||
import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
|
import { initializeProjectFactorStates, initializeProjectScaleState } from '@/lib/projectWorkspace'
|
||||||
import {
|
import {
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectIcon,
|
SelectIcon,
|
||||||
@ -76,6 +76,7 @@ interface ProjectInfoState {
|
|||||||
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
const PROJECT_INFO_KEY = 'xm-base-info-v1'
|
||||||
const PROJECT_CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
|
const PROJECT_CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
|
||||||
const PROJECT_MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
|
const PROJECT_MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
|
||||||
|
const PROJECT_SCALE_KEY = 'xm-info-v3'
|
||||||
const getActiveProjectId = () => readCurrentProjectId()
|
const getActiveProjectId = () => readCurrentProjectId()
|
||||||
|
|
||||||
const tabStore = useTabStore()
|
const tabStore = useTabStore()
|
||||||
@ -277,6 +278,7 @@ const confirmProjectCalc = async () => {
|
|||||||
PROJECT_CONSULT_CATEGORY_FACTOR_KEY,
|
PROJECT_CONSULT_CATEGORY_FACTOR_KEY,
|
||||||
PROJECT_MAJOR_FACTOR_KEY
|
PROJECT_MAJOR_FACTOR_KEY
|
||||||
)
|
)
|
||||||
|
await initializeProjectScaleState(kvAdapter, industry, PROJECT_SCALE_KEY)
|
||||||
writeWorkspaceMode('project')
|
writeWorkspaceMode('project')
|
||||||
window.location.href = buildProjectUrl(project.id, { forceHome: false, newProject: false })
|
window.location.href = buildProjectUrl(project.id, { forceHome: false, newProject: false })
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -443,45 +443,45 @@ watch(canUseLandScale, enabled => {
|
|||||||
|
|
||||||
|
|
||||||
<div class="quick-calc-toolbar">
|
<div class="quick-calc-toolbar">
|
||||||
<label class="quick-calc-toolbar__field quick-calc-toolbar__field--cards">
|
<section class="quick-calc-form-section quick-calc-toolbar__section">
|
||||||
<span class="quick-calc-field__label">{{ t('quickCalc.fields.industry') }}</span>
|
<div class="quick-calc-field quick-calc-field--wide">
|
||||||
<div class="quick-calc-industry-grid" role="radiogroup" :aria-label="t('quickCalc.fields.industry')">
|
<span class="quick-calc-field__label">{{ t('quickCalc.fields.industry') }}</span>
|
||||||
<label
|
<div class="quick-calc-industry-grid" role="radiogroup" :aria-label="t('quickCalc.fields.industry')">
|
||||||
v-for="item in industryTypeList"
|
<label
|
||||||
:key="`quick-workbench-${item.id}`"
|
v-for="item in industryTypeList"
|
||||||
class="quick-calc-industry-card"
|
:key="`quick-workbench-${item.id}`"
|
||||||
:class="{ 'is-selected': isIndustrySelected(item.id) }"
|
class="quick-calc-industry-card"
|
||||||
:aria-checked="isIndustrySelected(item.id)"
|
|
||||||
role="radio"
|
|
||||||
tabindex="0"
|
|
||||||
@click.prevent="handleIndustrySelect(item.id)"
|
|
||||||
@keydown.enter.prevent="handleIndustrySelect(item.id)"
|
|
||||||
@keydown.space.prevent="handleIndustrySelect(item.id)"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:checked="isIndustrySelected(item.id)"
|
|
||||||
type="radio"
|
|
||||||
name="quick-calc-industry-choice"
|
|
||||||
class="quick-calc-option__input"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="quick-calc-industry-card__icon"
|
|
||||||
:class="{ 'is-selected': isIndustrySelected(item.id) }"
|
:class="{ 'is-selected': isIndustrySelected(item.id) }"
|
||||||
|
:aria-checked="isIndustrySelected(item.id)"
|
||||||
|
role="radio"
|
||||||
|
tabindex="0"
|
||||||
|
@click.prevent="handleIndustrySelect(item.id)"
|
||||||
|
@keydown.enter.prevent="handleIndustrySelect(item.id)"
|
||||||
|
@keydown.space.prevent="handleIndustrySelect(item.id)"
|
||||||
>
|
>
|
||||||
<CircleDot v-if="isIndustrySelected(item.id)" class="h-3.5 w-3.5" />
|
<input
|
||||||
<Circle v-else class="h-3.5 w-3.5" />
|
:checked="isIndustrySelected(item.id)"
|
||||||
</span>
|
type="radio"
|
||||||
<span class="quick-calc-industry-card__text">
|
name="quick-calc-industry-choice"
|
||||||
{{ getIndustryDisplayName(item.id, locale) }}
|
class="quick-calc-option__input"
|
||||||
</span>
|
tabindex="-1"
|
||||||
</label>
|
>
|
||||||
|
<span
|
||||||
|
class="quick-calc-industry-card__icon"
|
||||||
|
:class="{ 'is-selected': isIndustrySelected(item.id) }"
|
||||||
|
>
|
||||||
|
<CircleDot v-if="isIndustrySelected(item.id)" class="h-3.5 w-3.5" />
|
||||||
|
<Circle v-else class="h-3.5 w-3.5" />
|
||||||
|
</span>
|
||||||
|
<span class="quick-calc-industry-card__text">
|
||||||
|
{{ getIndustryDisplayName(item.id, locale) }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
|
||||||
|
|
||||||
<span class="quick-calc-toolbar__meta">
|
|
||||||
{{ industrySaving ? t('quickCalc.saving') : industryLabel }}
|
</section>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!hasSelectedIndustry" class="quick-calc-empty-state">
|
<div v-if="!hasSelectedIndustry" class="quick-calc-empty-state">
|
||||||
@ -724,25 +724,18 @@ watch(canUseLandScale, enabled => {
|
|||||||
|
|
||||||
.quick-calc-toolbar {
|
.quick-calc-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: end;
|
align-items: stretch;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-bottom: 1px solid var(--qc-border);
|
border-bottom: 1px solid var(--qc-border);
|
||||||
background: color-mix(in srgb, var(--qc-surface-muted) 72%, transparent);
|
background: color-mix(in srgb, var(--qc-surface-muted) 72%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-calc-toolbar__field {
|
.quick-calc-toolbar__section {
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: min(280px, 100%);
|
min-width: min(280px, 100%);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-calc-toolbar__field--cards {
|
|
||||||
align-content: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-calc-industry-grid {
|
.quick-calc-industry-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(136px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(136px, 1fr));
|
||||||
@ -805,10 +798,8 @@ watch(canUseLandScale, enabled => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.quick-calc-toolbar__meta {
|
.quick-calc-toolbar__meta {
|
||||||
flex-shrink: 0;
|
justify-content: flex-end;
|
||||||
font-size: 12px;
|
|
||||||
color: var(--qc-muted);
|
color: var(--qc-muted);
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-calc-empty-state {
|
.quick-calc-empty-state {
|
||||||
@ -1383,7 +1374,7 @@ watch(canUseLandScale, enabled => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.quick-calc-toolbar__meta {
|
.quick-calc-toolbar__meta {
|
||||||
text-align: left;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-calc-layout {
|
.quick-calc-layout {
|
||||||
|
|||||||
@ -149,7 +149,7 @@ import {
|
|||||||
getServiceDictItemById,
|
getServiceDictItemById,
|
||||||
industryTypeList
|
industryTypeList
|
||||||
} from '@/sql'
|
} from '@/sql'
|
||||||
import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
|
import { initializeProjectFactorStates, initializeProjectScaleState } from '@/lib/projectWorkspace'
|
||||||
import { createProjectKvAdapter } from '@/lib/projectKvStore'
|
import { createProjectKvAdapter } from '@/lib/projectKvStore'
|
||||||
import { I18N_LOCALE_KEY, type AppLocale } from '@/i18n'
|
import { I18N_LOCALE_KEY, type AppLocale } from '@/i18n'
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
@ -539,6 +539,7 @@ const createProjectAndOpen = async () => {
|
|||||||
CONSULT_CATEGORY_FACTOR_DB_KEY,
|
CONSULT_CATEGORY_FACTOR_DB_KEY,
|
||||||
MAJOR_FACTOR_DB_KEY
|
MAJOR_FACTOR_DB_KEY
|
||||||
)
|
)
|
||||||
|
await initializeProjectScaleState(kvAdapter, industry, LEGACY_PROJECT_DB_KEY)
|
||||||
void refreshProjectList()
|
void refreshProjectList()
|
||||||
const href = buildProjectUrl(project.id)
|
const href = buildProjectUrl(project.id)
|
||||||
window.open(href, '_blank', 'noopener')
|
window.open(href, '_blank', 'noopener')
|
||||||
@ -992,6 +993,30 @@ const loadFactorRowsState = async (storageKey: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveExportFactorRows = (
|
||||||
|
primary: Awaited<ReturnType<typeof loadFactorRowsState>>,
|
||||||
|
fallback?: Awaited<ReturnType<typeof loadFactorRowsState>>
|
||||||
|
) => {
|
||||||
|
const primaryRows = Array.isArray(primary?.resolved?.detailRows) ? primary.resolved.detailRows : []
|
||||||
|
if (primaryRows.length > 0) return primaryRows
|
||||||
|
return Array.isArray(fallback?.resolved?.detailRows) ? fallback.resolved.detailRows : []
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveExportScaleState = <
|
||||||
|
TState extends { detailRows?: unknown[]; roughCalcEnabled?: unknown; totalAmount?: unknown } | null | undefined
|
||||||
|
>(
|
||||||
|
primary: TState,
|
||||||
|
fallback?: TState
|
||||||
|
) => {
|
||||||
|
const primaryRows = Array.isArray(primary?.detailRows) ? primary.detailRows : []
|
||||||
|
const primaryHasState =
|
||||||
|
primaryRows.length > 0
|
||||||
|
|| primary?.roughCalcEnabled === true
|
||||||
|
|| (typeof primary?.totalAmount === 'number' && Number.isFinite(primary.totalAmount))
|
||||||
|
if (primaryHasState) return primary
|
||||||
|
return fallback ?? primary ?? null
|
||||||
|
}
|
||||||
|
|
||||||
const createRichTextCode = (...parts: string[]): unknown => ({
|
const createRichTextCode = (...parts: string[]): unknown => ({
|
||||||
richText: parts
|
richText: parts
|
||||||
.map(item => String(item || '').trim())
|
.map(item => String(item || '').trim())
|
||||||
@ -1171,8 +1196,8 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
|||||||
const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumNumbers(projectScale.map(item => item.cost))
|
const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumNumbers(projectScale.map(item => item.cost))
|
||||||
|
|
||||||
|
|
||||||
const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorState.resolved?.detailRows)
|
const projectServiceCoes = buildProjectServiceCoes(resolveExportFactorRows(consultCategoryFactorState))
|
||||||
const projectMajorCoes = buildProjectMajorCoes(majorFactorState.resolved?.detailRows)
|
const projectMajorCoes = buildProjectMajorCoes(resolveExportFactorRows(majorFactorState))
|
||||||
const projectName = isNonEmptyString(projectInfo.projectName)
|
const projectName = isNonEmptyString(projectInfo.projectName)
|
||||||
? projectInfo.projectName.trim()
|
? projectInfo.projectName.trim()
|
||||||
: t('tab.messages.defaultProjectName')
|
: t('tab.messages.defaultProjectName')
|
||||||
@ -1369,10 +1394,15 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
|
|||||||
const addtionalFee = toMoney(addtional ? addtional.fee : 0)
|
const addtionalFee = toMoney(addtional ? addtional.fee : 0)
|
||||||
const reserveFee = toMoney(reserve ? reserve.fee : 0)
|
const reserveFee = toMoney(reserve ? reserve.fee : 0)
|
||||||
const contractFee = toMoney(addNumbers(serviceFee, addtionalFee, reserveFee))
|
const contractFee = toMoney(addNumbers(serviceFee, addtionalFee, reserveFee))
|
||||||
const contractScale = htInfoRaw?.roughCalcEnabled ? [] : toExportScaleRows(htInfoRaw?.detailRows)
|
const contractScaleState = resolveExportScaleState(htInfoRaw, projectScaleSource)
|
||||||
|
const contractScale = contractScaleState?.roughCalcEnabled ? [] : toExportScaleRows(contractScaleState?.detailRows)
|
||||||
|
|
||||||
const contractServiceCoesRaw = buildProjectServiceCoes(htConsultCategoryFactorState.resolved?.detailRows)
|
const contractServiceCoesRaw = buildProjectServiceCoes(
|
||||||
const contractMajorCoesRaw = buildProjectMajorCoes(htMajorFactorState.resolved?.detailRows)
|
resolveExportFactorRows(htConsultCategoryFactorState, consultCategoryFactorState)
|
||||||
|
)
|
||||||
|
const contractMajorCoesRaw = buildProjectMajorCoes(
|
||||||
|
resolveExportFactorRows(htMajorFactorState, majorFactorState)
|
||||||
|
)
|
||||||
console.log('[export][contract factor rows]', {
|
console.log('[export][contract factor rows]', {
|
||||||
contractId,
|
contractId,
|
||||||
consultFactorPinia: htConsultCategoryFactorState.piniaData,
|
consultFactorPinia: htConsultCategoryFactorState.piniaData,
|
||||||
|
|||||||
@ -37,7 +37,6 @@ export class AgGridResetHeader implements IHeaderComp {
|
|||||||
eButton.type = 'button'
|
eButton.type = 'button'
|
||||||
eButton.textContent = '↻'
|
eButton.textContent = '↻'
|
||||||
const fallbackResetTitle = i18n.global.t('agGrid.resetDefault')
|
const fallbackResetTitle = i18n.global.t('agGrid.resetDefault')
|
||||||
eButton.title = params.resetTitle || fallbackResetTitle
|
|
||||||
eButton.setAttribute('aria-label', params.resetTitle || fallbackResetTitle)
|
eButton.setAttribute('aria-label', params.resetTitle || fallbackResetTitle)
|
||||||
eButton.style.display = 'inline-flex'
|
eButton.style.display = 'inline-flex'
|
||||||
eButton.style.alignItems = 'center'
|
eButton.style.alignItems = 'center'
|
||||||
@ -70,7 +69,6 @@ export class AgGridResetHeader implements IHeaderComp {
|
|||||||
this.params = params
|
this.params = params
|
||||||
this.eLabel.textContent = params.displayName || params.column?.getColDef().headerName || ''
|
this.eLabel.textContent = params.displayName || params.column?.getColDef().headerName || ''
|
||||||
const fallbackResetTitle = i18n.global.t('agGrid.resetDefault')
|
const fallbackResetTitle = i18n.global.t('agGrid.resetDefault')
|
||||||
this.eButton.title = params.resetTitle || fallbackResetTitle
|
|
||||||
this.eButton.setAttribute('aria-label', params.resetTitle || fallbackResetTitle)
|
this.eButton.setAttribute('aria-label', params.resetTitle || fallbackResetTitle)
|
||||||
this.eButton.style.visibility = params.onReset ? 'visible' : 'hidden'
|
this.eButton.style.visibility = params.onReset ? 'visible' : 'hidden'
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { useKvStore } from '@/pinia/kv'
|
|||||||
|
|
||||||
interface StoredDetailRowsState<T = any> {
|
interface StoredDetailRowsState<T = any> {
|
||||||
detailRows?: T[]
|
detailRows?: T[]
|
||||||
|
totalAmount?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StoredFactorState {
|
interface StoredFactorState {
|
||||||
@ -36,6 +37,16 @@ const sumByNumberNullable = <T>(list: T[], pick: (item: T) => MaybeNumber): numb
|
|||||||
return hasValid ? total : null
|
return hasValid ? total : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getOnlyCostScaleSummaryAmount = (
|
||||||
|
rows?: Array<{ amount?: unknown; isGroupRow?: unknown }>,
|
||||||
|
totalAmount?: unknown
|
||||||
|
) => {
|
||||||
|
if (typeof totalAmount === 'number' && Number.isFinite(totalAmount)) return totalAmount
|
||||||
|
const summaryRow = (rows || []).find(row => row?.isGroupRow === true)
|
||||||
|
if (typeof summaryRow?.amount === 'number' && Number.isFinite(summaryRow.amount)) return summaryRow.amount
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
interface ScaleRow {
|
interface ScaleRow {
|
||||||
id: string
|
id: string
|
||||||
amount: number | null
|
amount: number | null
|
||||||
@ -388,9 +399,11 @@ const getOnlyCostScaleBudgetFee = (
|
|||||||
rowsFromDb: Array<Record<string, unknown>> | undefined,
|
rowsFromDb: Array<Record<string, unknown>> | undefined,
|
||||||
consultCategoryFactorMap?: Map<string, number | null>,
|
consultCategoryFactorMap?: Map<string, number | null>,
|
||||||
majorFactorMap?: Map<string, number | null>,
|
majorFactorMap?: Map<string, number | null>,
|
||||||
industryId?: string | null
|
industryId?: string | null,
|
||||||
|
totalAmount?: number | null
|
||||||
) => {
|
) => {
|
||||||
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
||||||
|
const rawRows = rowsFromDb || []
|
||||||
const sourceRows = stripGroupScaleRows(rowsFromDb)
|
const sourceRows = stripGroupScaleRows(rowsFromDb)
|
||||||
const defaultConsultCategoryFactor =
|
const defaultConsultCategoryFactor =
|
||||||
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
|
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
|
||||||
@ -421,12 +434,10 @@ const getOnlyCostScaleBudgetFee = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalAmount = sumByNumberNullable(sourceRows, row =>
|
const resolvedTotalAmount = getOnlyCostScaleSummaryAmount(rawRows as Array<{ amount?: unknown; isGroupRow?: unknown }>, totalAmount)
|
||||||
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
|
if (resolvedTotalAmount == null) return null
|
||||||
)
|
|
||||||
if (totalAmount == null) return null
|
|
||||||
const onlyRow =
|
const onlyRow =
|
||||||
sourceRows.find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
|
rawRows.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 = getRowNumberOrFallback(onlyRow, 'consultCategoryFactor', defaultConsultCategoryFactor)
|
const consultCategoryFactor = getRowNumberOrFallback(onlyRow, 'consultCategoryFactor', defaultConsultCategoryFactor)
|
||||||
@ -434,7 +445,7 @@ const getOnlyCostScaleBudgetFee = (
|
|||||||
const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1)
|
const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1)
|
||||||
const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 100)
|
const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 100)
|
||||||
return getScaleBudgetFeeByRow({
|
return getScaleBudgetFeeByRow({
|
||||||
amount: totalAmount,
|
amount: resolvedTotalAmount,
|
||||||
benchmarkBudgetBasicChecked: typeof onlyRow?.benchmarkBudgetBasicChecked === 'boolean' ? onlyRow.benchmarkBudgetBasicChecked : true,
|
benchmarkBudgetBasicChecked: typeof onlyRow?.benchmarkBudgetBasicChecked === 'boolean' ? onlyRow.benchmarkBudgetBasicChecked : true,
|
||||||
benchmarkBudgetOptionalChecked: typeof onlyRow?.benchmarkBudgetOptionalChecked === 'boolean' ? onlyRow.benchmarkBudgetOptionalChecked : true,
|
benchmarkBudgetOptionalChecked: typeof onlyRow?.benchmarkBudgetOptionalChecked === 'boolean' ? onlyRow.benchmarkBudgetOptionalChecked : true,
|
||||||
majorFactor,
|
majorFactor,
|
||||||
@ -449,16 +460,16 @@ const buildOnlyCostScaleDetailRows = (
|
|||||||
rowsFromDb: Array<Record<string, unknown>> | undefined,
|
rowsFromDb: Array<Record<string, unknown>> | undefined,
|
||||||
consultCategoryFactorMap?: Map<string, number | null>,
|
consultCategoryFactorMap?: Map<string, number | null>,
|
||||||
majorFactorMap?: Map<string, number | null>,
|
majorFactorMap?: Map<string, number | null>,
|
||||||
industryId?: string | null
|
industryId?: string | null,
|
||||||
|
totalAmount?: number | null
|
||||||
) => {
|
) => {
|
||||||
|
const rawRows = rowsFromDb || []
|
||||||
const sourceRows = stripGroupScaleRows(rowsFromDb)
|
const sourceRows = stripGroupScaleRows(rowsFromDb)
|
||||||
const totalAmount = sumByNumberNullable(sourceRows, row =>
|
const resolvedTotalAmount = getOnlyCostScaleSummaryAmount(rawRows as Array<{ amount?: unknown; isGroupRow?: unknown }>, totalAmount)
|
||||||
typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null
|
|
||||||
)
|
|
||||||
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
const industryMajorEntry = getIndustryMajorEntryByIndustryId(industryId)
|
||||||
const onlyCostRowId = industryMajorEntry?.id || ONLY_COST_SCALE_ROW_ID
|
const onlyCostRowId = industryMajorEntry?.id || ONLY_COST_SCALE_ROW_ID
|
||||||
const onlyRow =
|
const onlyRow =
|
||||||
sourceRows.find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
|
rawRows.find(row => String(row?.id || '') === ONLY_COST_SCALE_ROW_ID) ||
|
||||||
sourceRows.find(row => String(row?.id || '') === onlyCostRowId)
|
sourceRows.find(row => String(row?.id || '') === onlyCostRowId)
|
||||||
const consultCategoryFactor = getRowNumberOrFallback(
|
const consultCategoryFactor = getRowNumberOrFallback(
|
||||||
onlyRow,
|
onlyRow,
|
||||||
@ -478,7 +489,7 @@ const buildOnlyCostScaleDetailRows = (
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: onlyCostRowId,
|
id: onlyCostRowId,
|
||||||
amount: totalAmount,
|
amount: resolvedTotalAmount,
|
||||||
landArea: null,
|
landArea: null,
|
||||||
consultCategoryFactor,
|
consultCategoryFactor,
|
||||||
majorFactor,
|
majorFactor,
|
||||||
@ -735,7 +746,8 @@ const buildDefaultPricingMethodDetailRows = (
|
|||||||
context.htData?.detailRows as Array<Record<string, unknown>> | undefined,
|
context.htData?.detailRows as Array<Record<string, unknown>> | undefined,
|
||||||
context.consultCategoryFactorMap,
|
context.consultCategoryFactorMap,
|
||||||
context.majorFactorMap,
|
context.majorFactorMap,
|
||||||
context.industryId
|
context.industryId,
|
||||||
|
context.htData?.totalAmount ?? null
|
||||||
)
|
)
|
||||||
: scaleRows.filter(row => {
|
: scaleRows.filter(row => {
|
||||||
if (!isCostMajorById(row.id)) return false
|
if (!isCostMajorById(row.id)) return false
|
||||||
@ -846,7 +858,8 @@ export const getPricingMethodTotalsForService = async (params: {
|
|||||||
(htData?.detailRows as Array<Record<string, unknown>> | undefined),
|
(htData?.detailRows as Array<Record<string, unknown>> | undefined),
|
||||||
consultCategoryFactorMap,
|
consultCategoryFactorMap,
|
||||||
majorFactorMap,
|
majorFactorMap,
|
||||||
industryId
|
industryId,
|
||||||
|
htData?.totalAmount ?? null
|
||||||
)
|
)
|
||||||
: (() => {
|
: (() => {
|
||||||
const investRows = scopedInvestRows || resolveScaleRows(
|
const investRows = scopedInvestRows || resolveScaleRows(
|
||||||
|
|||||||
@ -9,6 +9,12 @@ type ScaleLinkRow = {
|
|||||||
path?: unknown
|
path?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScaleValueLinkRow = ScaleLinkRow & {
|
||||||
|
amount?: unknown
|
||||||
|
landArea?: unknown
|
||||||
|
isGroupRow?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeProjectCount = (value: unknown) => {
|
const normalizeProjectCount = (value: unknown) => {
|
||||||
const parsed = Number(value)
|
const parsed = Number(value)
|
||||||
if (!Number.isFinite(parsed)) return 1
|
if (!Number.isFinite(parsed)) return 1
|
||||||
@ -84,6 +90,55 @@ export const buildContractScaleIdMap = <TRow extends ScaleLinkRow>(rows: TRow[]
|
|||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const buildContractScaleProjectTotals = <TRow extends ScaleValueLinkRow>(
|
||||||
|
rows: TRow[] | undefined,
|
||||||
|
totalAmount?: unknown
|
||||||
|
) => {
|
||||||
|
const map = new Map<number, { amount: number | null; landArea: number | null }>()
|
||||||
|
|
||||||
|
const normalizedTotalAmount =
|
||||||
|
typeof totalAmount === 'number' && Number.isFinite(totalAmount)
|
||||||
|
? totalAmount
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (normalizedTotalAmount != null) {
|
||||||
|
map.set(1, {
|
||||||
|
amount: normalizedTotalAmount,
|
||||||
|
landArea: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of rows || []) {
|
||||||
|
if (row?.isGroupRow !== true) continue
|
||||||
|
const projectIndex = resolveScaleRowProjectIndex(row)
|
||||||
|
const current = map.get(projectIndex) || { amount: null, landArea: null }
|
||||||
|
const nextAmount =
|
||||||
|
current.amount != null
|
||||||
|
? current.amount
|
||||||
|
: (typeof row?.amount === 'number' && Number.isFinite(row.amount) ? row.amount : null)
|
||||||
|
const nextLandArea =
|
||||||
|
current.landArea != null
|
||||||
|
? current.landArea
|
||||||
|
: (typeof row?.landArea === 'number' && Number.isFinite(row.landArea) ? row.landArea : null)
|
||||||
|
map.set(projectIndex, {
|
||||||
|
amount: nextAmount,
|
||||||
|
landArea: nextLandArea
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getContractScaleProjectTotalsByRow = (
|
||||||
|
row: ScaleLinkRow | undefined,
|
||||||
|
totalsMap: Map<number, { amount: number | null; landArea: number | null }>
|
||||||
|
) => {
|
||||||
|
const projectIndex = resolveScaleRowProjectIndex(row)
|
||||||
|
return totalsMap.get(projectIndex)
|
||||||
|
|| (projectIndex > 1 ? totalsMap.get(1) : undefined)
|
||||||
|
|| { amount: null, landArea: null }
|
||||||
|
}
|
||||||
|
|
||||||
export const getContractScaleRowByMajor = <TRow extends ScaleLinkRow>(
|
export const getContractScaleRowByMajor = <TRow extends ScaleLinkRow>(
|
||||||
row: ScaleLinkRow,
|
row: ScaleLinkRow,
|
||||||
map: Map<string, TRow>,
|
map: Map<string, TRow>,
|
||||||
|
|||||||
@ -15,6 +15,8 @@ type DictItemLite = {
|
|||||||
name?: string
|
name?: string
|
||||||
defCoe?: number | null
|
defCoe?: number | null
|
||||||
notshowByzxflxs?: boolean
|
notshowByzxflxs?: boolean
|
||||||
|
hasCost?: boolean | null
|
||||||
|
hasArea?: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type FactorPersistRow = {
|
type FactorPersistRow = {
|
||||||
@ -31,6 +33,48 @@ type FactorPersistState = {
|
|||||||
detailRows: FactorPersistRow[]
|
detailRows: FactorPersistRow[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScalePersistRow = {
|
||||||
|
id: string
|
||||||
|
groupCode: string
|
||||||
|
groupName: string
|
||||||
|
majorCode: string
|
||||||
|
majorName: string
|
||||||
|
hasCost: boolean
|
||||||
|
hasArea: boolean
|
||||||
|
amount: number | null
|
||||||
|
landArea: number | null
|
||||||
|
path: string[]
|
||||||
|
hide?: boolean
|
||||||
|
isGroupRow?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScalePersistState = {
|
||||||
|
detailRows: ScalePersistRow[]
|
||||||
|
totalAmount: number | null
|
||||||
|
roughCalcEnabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScalePersistStateSource = {
|
||||||
|
detailRows?: unknown[]
|
||||||
|
totalAmount?: unknown
|
||||||
|
roughCalcEnabled?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScaleDictLeaf = {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
hasCost: boolean
|
||||||
|
hasArea: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScaleDictGroup = {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
children: ScaleDictLeaf[]
|
||||||
|
}
|
||||||
|
|
||||||
const buildCodePath = (code: string, selfId: string, codeIdMap: Map<string, string>) => {
|
const buildCodePath = (code: string, selfId: string, codeIdMap: Map<string, string>) => {
|
||||||
const parts = code.split('-').filter(Boolean)
|
const parts = code.split('-').filter(Boolean)
|
||||||
if (!parts.length) return [selfId]
|
if (!parts.length) return [selfId]
|
||||||
@ -80,6 +124,151 @@ const buildFactorRowsFromEntries = (entries: Array<{ id: string; item: DictItemL
|
|||||||
.filter((item): item is FactorPersistRow => Boolean(item))
|
.filter((item): item is FactorPersistRow => Boolean(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildScaleGroupsFromEntries = (entries: Array<{ id: string; item: DictItemLite }>) => {
|
||||||
|
const groupMap = new Map<string, ScaleDictGroup>()
|
||||||
|
const groupOrder: string[] = []
|
||||||
|
const codeLookup = new Map(entries.map(entry => [String(entry.item?.code || '').trim(), entry]))
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const code = String(entry.item?.code || '').trim()
|
||||||
|
const name = String(entry.item?.name || '').trim()
|
||||||
|
if (!code || !name) continue
|
||||||
|
|
||||||
|
if (!code.includes('-')) {
|
||||||
|
if (!groupMap.has(code)) groupOrder.push(code)
|
||||||
|
groupMap.set(code, {
|
||||||
|
id: entry.id,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
children: []
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentCode = code.split('-')[0]
|
||||||
|
if (!groupMap.has(parentCode)) {
|
||||||
|
const parent = codeLookup.get(parentCode)
|
||||||
|
if (!groupOrder.includes(parentCode)) groupOrder.push(parentCode)
|
||||||
|
groupMap.set(parentCode, {
|
||||||
|
id: parent?.id || `group-${parentCode}`,
|
||||||
|
code: parentCode,
|
||||||
|
name: String(parent?.item?.name || parentCode).trim(),
|
||||||
|
children: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
groupMap.get(parentCode)?.children.push({
|
||||||
|
id: entry.id,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
hasCost: entry.item?.hasCost !== false,
|
||||||
|
hasArea: entry.item?.hasArea !== false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupOrder
|
||||||
|
.map(code => groupMap.get(code))
|
||||||
|
.filter((item): item is ScaleDictGroup => Boolean(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildScaleLeafRows = (groups: ScaleDictGroup[]): ScalePersistRow[] => (
|
||||||
|
groups.flatMap(group =>
|
||||||
|
group.children.map(child => ({
|
||||||
|
id: child.id,
|
||||||
|
groupCode: group.code,
|
||||||
|
groupName: group.name,
|
||||||
|
majorCode: child.code,
|
||||||
|
majorName: child.name,
|
||||||
|
hasCost: child.hasCost,
|
||||||
|
hasArea: child.hasArea,
|
||||||
|
amount: null,
|
||||||
|
landArea: null,
|
||||||
|
path: [`${group.code} ${group.name}`, `${child.code} ${child.name}`]
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const mergeScaleLeafRows = (defaultRows: ScalePersistRow[], rowsFromDb: ScalePersistRow[] | undefined) => {
|
||||||
|
const dbValueMap = new Map<string, ScalePersistRow>()
|
||||||
|
for (const row of rowsFromDb || []) {
|
||||||
|
if (!row || row.isGroupRow === true) continue
|
||||||
|
dbValueMap.set(String(row.id || ''), row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultRows.map(row => {
|
||||||
|
const fromDb = dbValueMap.get(row.id)
|
||||||
|
if (!fromDb) return row
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
hide: fromDb.hide,
|
||||||
|
amount: row.hasCost && typeof fromDb.amount === 'number' && Number.isFinite(fromDb.amount) ? fromDb.amount : null,
|
||||||
|
landArea: row.hasArea && typeof fromDb.landArea === 'number' && Number.isFinite(fromDb.landArea) ? fromDb.landArea : null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildScaleGroupRows = (groups: ScaleDictGroup[], rows: ScalePersistRow[]): ScalePersistRow[] => {
|
||||||
|
const rowById = new Map(rows.map(row => [String(row.id || ''), row] as const))
|
||||||
|
|
||||||
|
return groups.map(group => {
|
||||||
|
let amountTotal = 0
|
||||||
|
let hasAmount = false
|
||||||
|
let landAreaTotal = 0
|
||||||
|
let hasLandArea = false
|
||||||
|
|
||||||
|
for (const child of group.children) {
|
||||||
|
const leaf = rowById.get(String(child.id || ''))
|
||||||
|
const amount = leaf?.amount
|
||||||
|
if (typeof amount === 'number' && Number.isFinite(amount)) {
|
||||||
|
amountTotal += amount
|
||||||
|
hasAmount = true
|
||||||
|
}
|
||||||
|
const landArea = leaf?.landArea
|
||||||
|
if (typeof landArea === 'number' && Number.isFinite(landArea)) {
|
||||||
|
landAreaTotal += landArea
|
||||||
|
hasLandArea = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: group.id,
|
||||||
|
groupCode: group.code,
|
||||||
|
groupName: group.name,
|
||||||
|
majorCode: group.code,
|
||||||
|
majorName: group.name,
|
||||||
|
hasCost: true,
|
||||||
|
hasArea: true,
|
||||||
|
amount: hasAmount ? amountTotal : null,
|
||||||
|
landArea: hasLandArea ? landAreaTotal : null,
|
||||||
|
path: [`${group.code} ${group.name}`],
|
||||||
|
hide: false,
|
||||||
|
isGroupRow: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const buildDefaultProjectScaleState = (industry: string, source?: ScalePersistStateSource | null): ScalePersistState => {
|
||||||
|
const majorEntries = getMajorDictEntries()
|
||||||
|
.map(({ id, item }) => ({ id, item: item as DictItemLite }))
|
||||||
|
.filter(({ id, item }) => item.notshowByzxflxs !== true && isMajorIdInIndustryScope(id, industry))
|
||||||
|
|
||||||
|
const groups = buildScaleGroupsFromEntries(majorEntries)
|
||||||
|
const defaultLeafRows = buildScaleLeafRows(groups)
|
||||||
|
const detailRows = mergeScaleLeafRows(
|
||||||
|
defaultLeafRows,
|
||||||
|
Array.isArray(source?.detailRows) ? source.detailRows as ScalePersistRow[] : undefined
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
detailRows: [...detailRows, ...buildScaleGroupRows(groups, detailRows)],
|
||||||
|
totalAmount:
|
||||||
|
typeof source?.totalAmount === 'number' && Number.isFinite(source.totalAmount)
|
||||||
|
? source.totalAmount
|
||||||
|
: null,
|
||||||
|
roughCalcEnabled: source?.roughCalcEnabled === true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const initializeProjectFactorStates = async (
|
export const initializeProjectFactorStates = async (
|
||||||
kvStore: KvStoreLike,
|
kvStore: KvStoreLike,
|
||||||
industry: string,
|
industry: string,
|
||||||
@ -110,3 +299,11 @@ export const initializeProjectFactorStates = async (
|
|||||||
kvStore.setItem(majorFactorKey, majorPayload)
|
kvStore.setItem(majorFactorKey, majorPayload)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const initializeProjectScaleState = async (
|
||||||
|
kvStore: KvStoreLike,
|
||||||
|
industry: string,
|
||||||
|
projectScaleKey: string
|
||||||
|
) => {
|
||||||
|
await kvStore.setItem(projectScaleKey, buildDefaultProjectScaleState(industry))
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,9 @@ export { toFiniteNumber, toFiniteNumberOrZero }
|
|||||||
interface ScaleMethodRowLike {
|
interface ScaleMethodRowLike {
|
||||||
id: string
|
id: string
|
||||||
majorDictId?: unknown
|
majorDictId?: unknown
|
||||||
|
hasCost?: unknown
|
||||||
|
hasArea?: unknown
|
||||||
|
isGroupRow?: unknown
|
||||||
amount: number | null
|
amount: number | null
|
||||||
landArea: number | null
|
landArea: number | null
|
||||||
benchmarkBudgetBasicChecked?: unknown
|
benchmarkBudgetBasicChecked?: unknown
|
||||||
@ -33,12 +36,16 @@ interface WorkContentRowLike {
|
|||||||
|
|
||||||
interface FactorRowLike {
|
interface FactorRowLike {
|
||||||
id: string
|
id: string
|
||||||
|
standardFactor?: unknown
|
||||||
budgetValue?: unknown
|
budgetValue?: unknown
|
||||||
remark?: unknown
|
remark?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScaleRowLike {
|
interface ScaleRowLike {
|
||||||
id: string
|
id: string
|
||||||
|
hasCost?: unknown
|
||||||
|
hasArea?: unknown
|
||||||
|
isGroupRow?: unknown
|
||||||
amount: number | null
|
amount: number | null
|
||||||
landArea: number | null
|
landArea: number | null
|
||||||
}
|
}
|
||||||
@ -89,7 +96,6 @@ interface QuantityMethodRowLike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ExportScaleRowLike {
|
interface ExportScaleRowLike {
|
||||||
majorid: number
|
|
||||||
major: number
|
major: number
|
||||||
cost: number | null
|
cost: number | null
|
||||||
area: number | null
|
area: number | null
|
||||||
@ -249,6 +255,7 @@ const dedupeScaleMethodRows = (rows: ScaleMethodRowLike[] | undefined): ScaleMet
|
|||||||
if (!Array.isArray(rows) || rows.length === 0) return []
|
if (!Array.isArray(rows) || rows.length === 0) return []
|
||||||
const deduped = new Map<string, ScaleMethodRowLike>()
|
const deduped = new Map<string, ScaleMethodRowLike>()
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
if (row?.isGroupRow === true) continue
|
||||||
const major = toScaleMajorId(row)
|
const major = toScaleMajorId(row)
|
||||||
if (major == null) continue
|
if (major == null) continue
|
||||||
const proNum = toScaleProNum(row)
|
const proNum = toScaleProNum(row)
|
||||||
@ -258,6 +265,20 @@ const dedupeScaleMethodRows = (rows: ScaleMethodRowLike[] | undefined): ScaleMet
|
|||||||
return Array.from(deduped.values())
|
return Array.from(deduped.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isScaleLeafRow = (row: { isGroupRow?: unknown; hasCost?: unknown; hasArea?: unknown } | null | undefined) => {
|
||||||
|
if (!row || row.isGroupRow === true) return false
|
||||||
|
if (row.hasCost === true || row.hasArea === true) return true
|
||||||
|
return row.hasCost == null && row.hasArea == null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExportableScaleMethodRow = (
|
||||||
|
row: ScaleMethodRowLike | null | undefined,
|
||||||
|
mode: 'cost' | 'area'
|
||||||
|
) => {
|
||||||
|
if (!isScaleLeafRow(row)) return false
|
||||||
|
return mode === 'cost' ? row?.hasCost === true : row?.hasArea === true
|
||||||
|
}
|
||||||
|
|
||||||
export const normalizeTaskText = (value: unknown): string => String(value || '').trim()
|
export const normalizeTaskText = (value: unknown): string => String(value || '').trim()
|
||||||
|
|
||||||
export const resolveTaskRowServiceId = (row: WorkContentRowLike): number | null =>
|
export const resolveTaskRowServiceId = (row: WorkContentRowLike): number | null =>
|
||||||
@ -361,11 +382,11 @@ export const buildProjectServiceCoes = (rows: FactorRowLike[] | undefined) => {
|
|||||||
return rows
|
return rows
|
||||||
.map(row => {
|
.map(row => {
|
||||||
const serviceid = toSafeInteger(row.id)
|
const serviceid = toSafeInteger(row.id)
|
||||||
if (serviceid == null || row.budgetValue == null) return null
|
const coe = toFiniteNumber(row.budgetValue) ?? toFiniteNumber(row.standardFactor)
|
||||||
const coe = toFiniteNumber(row.budgetValue)
|
if (serviceid == null || coe == null) return null
|
||||||
return {
|
return {
|
||||||
serviceid,
|
serviceid,
|
||||||
coe: coe ?? 0,
|
coe,
|
||||||
remark: typeof row.remark === 'string' ? row.remark : ''
|
remark: typeof row.remark === 'string' ? row.remark : ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -377,11 +398,11 @@ export const buildProjectMajorCoes = (rows: FactorRowLike[] | undefined) => {
|
|||||||
return rows
|
return rows
|
||||||
.map(row => {
|
.map(row => {
|
||||||
const majorid = toSafeInteger(row.id)
|
const majorid = toSafeInteger(row.id)
|
||||||
if (majorid == null || row.budgetValue == null) return null
|
const coe = toFiniteNumber(row.budgetValue) ?? toFiniteNumber(row.standardFactor)
|
||||||
const coe = toFiniteNumber(row.budgetValue)
|
if (majorid == null || coe == null) return null
|
||||||
return {
|
return {
|
||||||
majorid,
|
majorid,
|
||||||
coe: coe ?? 0,
|
coe,
|
||||||
remark: typeof row.remark === 'string' ? row.remark : ''
|
remark: typeof row.remark === 'string' ? row.remark : ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -391,11 +412,11 @@ export const buildProjectMajorCoes = (rows: FactorRowLike[] | undefined) => {
|
|||||||
export const toExportScaleRows = (rows: ScaleRowLike[] | undefined) => {
|
export const toExportScaleRows = (rows: ScaleRowLike[] | undefined) => {
|
||||||
if (!Array.isArray(rows)) return []
|
if (!Array.isArray(rows)) return []
|
||||||
return rows
|
return rows
|
||||||
|
.filter(row => isScaleLeafRow(row))
|
||||||
.map(row => {
|
.map(row => {
|
||||||
const majorid = toSafeInteger(row.id)
|
const majorid = toSafeInteger(row.id)
|
||||||
if (majorid == null || (row.amount == null && row.landArea == null)) return null
|
if (majorid == null || (row.amount == null && row.landArea == null)) return null
|
||||||
return {
|
return {
|
||||||
majorid,
|
|
||||||
major: majorid,
|
major: majorid,
|
||||||
cost: row.amount,
|
cost: row.amount,
|
||||||
area: row.landArea
|
area: row.landArea
|
||||||
@ -411,6 +432,7 @@ export const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined) => {
|
|||||||
const proSet = new Set<number>()
|
const proSet = new Set<number>()
|
||||||
const det = normalizedRows
|
const det = normalizedRows
|
||||||
.map(row => {
|
.map(row => {
|
||||||
|
if (!isExportableScaleMethodRow(row, 'cost')) return null
|
||||||
const major = toScaleMajorId(row)
|
const major = toScaleMajorId(row)
|
||||||
if (major == null) return null
|
if (major == null) return null
|
||||||
const proNum = toScaleProNum(row)
|
const proNum = toScaleProNum(row)
|
||||||
@ -461,6 +483,7 @@ export const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined) => {
|
|||||||
const proSet = new Set<number>()
|
const proSet = new Set<number>()
|
||||||
const det = normalizedRows
|
const det = normalizedRows
|
||||||
.map(row => {
|
.map(row => {
|
||||||
|
if (!isExportableScaleMethodRow(row, 'area')) return null
|
||||||
const major = toScaleMajorId(row)
|
const major = toScaleMajorId(row)
|
||||||
if (major == null) return null
|
if (major == null) return null
|
||||||
const proNum = toScaleProNum(row)
|
const proNum = toScaleProNum(row)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
||||||
import { ensurePricingMethodDetailRowsForServices } from '@/lib/pricingMethodTotals'
|
import { ensurePricingMethodDetailRowsForServices } from '@/lib/pricingMethodTotals'
|
||||||
|
import { getServiceDictItemById } from '@/sql'
|
||||||
import {
|
import {
|
||||||
isSameNullableNumber,
|
isSameNullableNumber,
|
||||||
isSameScaleDetailRow,
|
isSameScaleDetailRow,
|
||||||
@ -8,14 +9,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
buildContractScaleIdMap,
|
buildContractScaleIdMap,
|
||||||
buildContractScaleMap,
|
buildContractScaleMap,
|
||||||
|
buildContractScaleProjectTotals,
|
||||||
getContractScaleRowByMajor,
|
getContractScaleRowByMajor,
|
||||||
|
getContractScaleProjectTotalsByRow,
|
||||||
normalizeChangedScaleRowIds,
|
normalizeChangedScaleRowIds,
|
||||||
parseScopedRowId,
|
|
||||||
resolveScaleRowMajorDictId as resolveRowMajorDictId
|
resolveScaleRowMajorDictId as resolveRowMajorDictId
|
||||||
} from '@/lib/pricingScaleLink'
|
} from '@/lib/pricingScaleLink'
|
||||||
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
import { loadConsultCategoryFactorMap, loadMajorFactorMap } from '@/lib/xmFactorDefaults'
|
||||||
import { useKvStore } from '@/pinia/kv'
|
import { useKvStore } from '@/pinia/kv'
|
||||||
import { getServiceDictItemById } from '@/sql'
|
|
||||||
import { useZxFwPricingStore, type ServicePricingMethod, type ZxFwPricingField } from '@/pinia/zxFwPricing'
|
import { useZxFwPricingStore, type ServicePricingMethod, type ZxFwPricingField } from '@/pinia/zxFwPricing'
|
||||||
|
|
||||||
export type { ZxFwPricingField } from '@/pinia/zxFwPricing'
|
export type { ZxFwPricingField } from '@/pinia/zxFwPricing'
|
||||||
@ -23,11 +24,6 @@ export type { ZxFwPricingField } from '@/pinia/zxFwPricing'
|
|||||||
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
|
const PRICING_TOTALS_OPTIONS = { excludeInvestmentCostAndAreaRows: true } as const
|
||||||
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
|
const ONLY_COST_SCALE_ROW_ID = '__only-cost-scale-total__'
|
||||||
|
|
||||||
type ServiceLite = {
|
|
||||||
mutiple?: boolean | null
|
|
||||||
onlyCostScale?: boolean | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScaleDetailRow = {
|
type ScaleDetailRow = {
|
||||||
id: string
|
id: string
|
||||||
projectIndex?: number
|
projectIndex?: number
|
||||||
@ -51,9 +47,6 @@ type ScaleDetailRow = {
|
|||||||
path?: string[]
|
path?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const calcOnlyCostScaleAmountFromRows = (rows?: Array<{ amount?: unknown }>) =>
|
|
||||||
sumByNumber(rows || [], row => (typeof row?.amount === 'number' ? row.amount : null))
|
|
||||||
|
|
||||||
const getScaleMethodTotalBudgetFee = (rows: ScaleDetailRow[]) => {
|
const getScaleMethodTotalBudgetFee = (rows: ScaleDetailRow[]) => {
|
||||||
const hasValue = rows.some(row => typeof row.budgetFee === 'number' && Number.isFinite(row.budgetFee))
|
const hasValue = rows.some(row => typeof row.budgetFee === 'number' && Number.isFinite(row.budgetFee))
|
||||||
if (!hasValue) return null
|
if (!hasValue) return null
|
||||||
@ -70,6 +63,10 @@ type WorkloadDetailRow = {
|
|||||||
serviceFee?: number | null
|
serviceFee?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServiceLite = {
|
||||||
|
onlyCostScale?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeServiceIdSet = (serviceIds?: Array<string | number>) =>
|
const normalizeServiceIdSet = (serviceIds?: Array<string | number>) =>
|
||||||
new Set((serviceIds || []).map(id => String(id || '').trim()).filter(Boolean))
|
new Set((serviceIds || []).map(id => String(id || '').trim()).filter(Boolean))
|
||||||
|
|
||||||
@ -111,12 +108,18 @@ const getWorkloadMethodTotalServiceFee = (rows: WorkloadDetailRow[]) => {
|
|||||||
return roundTo(sumByNumber(rows, row => row.serviceFee ?? null), 2)
|
return roundTo(sumByNumber(rows, row => row.serviceFee ?? null), 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchesChangedScaleRow = (row: ScaleDetailRow, changedRowIds?: Set<string>) => {
|
const isOnlyCostScaleService = (serviceId: string) => {
|
||||||
|
const service = getServiceDictItemById(serviceId) as ServiceLite | undefined
|
||||||
|
return service?.onlyCostScale === true
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchesChangedScaleRow = (
|
||||||
|
row: ScaleDetailRow,
|
||||||
|
changedRowIds?: Set<string>,
|
||||||
|
options?: { bypassFilter?: boolean }
|
||||||
|
) => {
|
||||||
|
if (options?.bypassFilter) return true
|
||||||
if (!changedRowIds || changedRowIds.size === 0) return true
|
if (!changedRowIds || changedRowIds.size === 0) return true
|
||||||
const directRowId = String(row.id || '').trim()
|
|
||||||
if (directRowId && changedRowIds.has(directRowId)) return true
|
|
||||||
const parsedMajorId = parseScopedRowId(row.id).majorDictId
|
|
||||||
if (parsedMajorId && changedRowIds.has(parsedMajorId)) return true
|
|
||||||
const majorDictId = resolveRowMajorDictId(row)
|
const majorDictId = resolveRowMajorDictId(row)
|
||||||
return Boolean(majorDictId && changedRowIds.has(majorDictId))
|
return Boolean(majorDictId && changedRowIds.has(majorDictId))
|
||||||
}
|
}
|
||||||
@ -127,9 +130,8 @@ const syncScaleMethodRows = async (params: {
|
|||||||
method: ServicePricingMethod
|
method: ServicePricingMethod
|
||||||
sourceRowMap: Map<string, ScaleDetailRow>
|
sourceRowMap: Map<string, ScaleDetailRow>
|
||||||
sourceRowIdMap: Map<string, ScaleDetailRow>
|
sourceRowIdMap: Map<string, ScaleDetailRow>
|
||||||
|
projectTotals: Map<number, { amount: number | null; landArea: number | null }>
|
||||||
changedRowIds?: Set<string>
|
changedRowIds?: Set<string>
|
||||||
onlyCostScaleFallbackAmount?: number | null
|
|
||||||
isOnlyCostScaleService?: boolean
|
|
||||||
}) => {
|
}) => {
|
||||||
const store = useZxFwPricingStore()
|
const store = useZxFwPricingStore()
|
||||||
const methodState = await store.loadServicePricingMethodState<ScaleDetailRow>(
|
const methodState = await store.loadServicePricingMethodState<ScaleDetailRow>(
|
||||||
@ -141,30 +143,25 @@ const syncScaleMethodRows = async (params: {
|
|||||||
|
|
||||||
let changed = false
|
let changed = false
|
||||||
let changedRowCount = 0
|
let changedRowCount = 0
|
||||||
|
const useSummaryScaleValues =
|
||||||
|
methodState.detailRows.length === 1 ||
|
||||||
|
(params.method === 'investScale' && isOnlyCostScaleService(params.serviceId))
|
||||||
const nextRows = methodState.detailRows.map(rawRow => {
|
const nextRows = methodState.detailRows.map(rawRow => {
|
||||||
const mode = params.method === 'investScale' ? 'cost' : 'area'
|
const mode = params.method === 'investScale' ? 'cost' : 'area'
|
||||||
if (!matchesChangedScaleRow(rawRow, params.changedRowIds)) return rawRow
|
if (!matchesChangedScaleRow(rawRow, params.changedRowIds, { bypassFilter: useSummaryScaleValues })) return rawRow
|
||||||
const row = { ...rawRow }
|
const row = { ...rawRow }
|
||||||
const sourceRow = getContractScaleRowByMajor(row, params.sourceRowMap, params.sourceRowIdMap)
|
const sourceRow = getContractScaleRowByMajor(row, params.sourceRowMap, params.sourceRowIdMap)
|
||||||
|
const projectTotals = getContractScaleProjectTotalsByRow(row, params.projectTotals)
|
||||||
|
|
||||||
if (mode === 'cost') {
|
if (mode === 'cost') {
|
||||||
const nextAmount = params.isOnlyCostScaleService
|
const nextAmount = useSummaryScaleValues
|
||||||
? (
|
? projectTotals.amount
|
||||||
typeof sourceRow?.amount === 'number'
|
: (typeof sourceRow?.amount === 'number' ? sourceRow.amount : null)
|
||||||
? sourceRow.amount
|
|
||||||
: (params.onlyCostScaleFallbackAmount ?? null)
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
typeof sourceRow?.amount === 'number'
|
|
||||||
? sourceRow.amount
|
|
||||||
: null
|
|
||||||
)
|
|
||||||
row.amount = isSameNullableNumber(row.amount, nextAmount) ? row.amount : nextAmount
|
row.amount = isSameNullableNumber(row.amount, nextAmount) ? row.amount : nextAmount
|
||||||
} else {
|
} else {
|
||||||
const nextLandArea =
|
const nextLandArea = useSummaryScaleValues
|
||||||
typeof sourceRow?.landArea === 'number'
|
? projectTotals.landArea
|
||||||
? sourceRow.landArea
|
: (typeof sourceRow?.landArea === 'number' ? sourceRow.landArea : null)
|
||||||
: null
|
|
||||||
row.landArea = isSameNullableNumber(row.landArea, nextLandArea) ? row.landArea : nextLandArea
|
row.landArea = isSameNullableNumber(row.landArea, nextLandArea) ? row.landArea : nextLandArea
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,26 +233,24 @@ export const syncContractScaleToPricing = async (
|
|||||||
options: PRICING_TOTALS_OPTIONS
|
options: PRICING_TOTALS_OPTIONS
|
||||||
})
|
})
|
||||||
|
|
||||||
const htData = await kvStore.getItem<{ detailRows?: ScaleDetailRow[] }>(`ht-info-v3-${contractId}`)
|
const htData = await kvStore.getItem<{ detailRows?: ScaleDetailRow[]; totalAmount?: number | null }>(`ht-info-v3-${contractId}`)
|
||||||
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
const sourceRows = Array.isArray(htData?.detailRows) ? htData.detailRows : []
|
||||||
const sourceRowMap = buildContractScaleMap(sourceRows)
|
const sourceRowMap = buildContractScaleMap(sourceRows)
|
||||||
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
const sourceRowIdMap = buildContractScaleIdMap(sourceRows)
|
||||||
const onlyCostScaleFallbackAmount = calcOnlyCostScaleAmountFromRows(sourceRows)
|
const projectTotals = buildContractScaleProjectTotals(sourceRows, htData?.totalAmount)
|
||||||
const updatedServiceIdSet = new Set<string>()
|
const updatedServiceIdSet = new Set<string>()
|
||||||
let updatedMethodCount = 0
|
let updatedMethodCount = 0
|
||||||
let updatedRowCount = 0
|
let updatedRowCount = 0
|
||||||
|
|
||||||
for (const serviceId of selectedIds) {
|
for (const serviceId of selectedIds) {
|
||||||
const service = getServiceDictItemById(serviceId) as ServiceLite | undefined
|
|
||||||
const investChangedCount = await syncScaleMethodRows({
|
const investChangedCount = await syncScaleMethodRows({
|
||||||
contractId,
|
contractId,
|
||||||
serviceId,
|
serviceId,
|
||||||
method: 'investScale',
|
method: 'investScale',
|
||||||
sourceRowMap,
|
sourceRowMap,
|
||||||
sourceRowIdMap,
|
sourceRowIdMap,
|
||||||
changedRowIds: changedRowIdSet,
|
projectTotals,
|
||||||
onlyCostScaleFallbackAmount,
|
changedRowIds: changedRowIdSet
|
||||||
isOnlyCostScaleService: service?.onlyCostScale === true
|
|
||||||
})
|
})
|
||||||
if (investChangedCount > 0) {
|
if (investChangedCount > 0) {
|
||||||
updatedServiceIdSet.add(serviceId)
|
updatedServiceIdSet.add(serviceId)
|
||||||
@ -268,6 +263,7 @@ export const syncContractScaleToPricing = async (
|
|||||||
method: 'landScale',
|
method: 'landScale',
|
||||||
sourceRowMap,
|
sourceRowMap,
|
||||||
sourceRowIdMap,
|
sourceRowIdMap,
|
||||||
|
projectTotals,
|
||||||
changedRowIds: changedRowIdSet
|
changedRowIds: changedRowIdSet
|
||||||
})
|
})
|
||||||
if (landChangedCount > 0) {
|
if (landChangedCount > 0) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user