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