This commit is contained in:
wintsa 2026-03-27 09:55:08 +08:00
parent b1728bbc47
commit bbcd07a595
5 changed files with 207 additions and 116 deletions

View File

@ -18,7 +18,7 @@ import {
} from '@/lib/workspace' } from '@/lib/workspace'
import { collectActiveProjectSessionLocks, initProjectSessionLock } from '@/lib/projectSessionLock' import { collectActiveProjectSessionLocks, initProjectSessionLock } from '@/lib/projectSessionLock'
import { listenProjectDeleted, listenResetAll } from '@/lib/projectEvents' import { listenProjectDeleted, listenResetAll } from '@/lib/projectEvents'
import { createProject, listProjects, type ProjectMeta } from '@/lib/projectRegistry' import { listProjects, type ProjectMeta } from '@/lib/projectRegistry'
const tabStore = useTabStore() const tabStore = useTabStore()
const { t } = useI18n() const { t } = useI18n()
@ -130,9 +130,8 @@ const openProjectInNewTab = (projectId: string, options?: { newProject?: boolean
} }
const createProjectAndOpen = () => { const createProjectAndOpen = () => {
const project = createProject(t('xmInfo.defaultProjectName'))
refreshConflictProjectList() refreshConflictProjectList()
openProjectInNewTab(project.id, { newProject: true }) openProjectInNewTab(DEFAULT_PROJECT_ID, { newProject: true })
} }
const syncRouteRequestFlags = () => { const syncRouteRequestFlags = () => {

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@ -47,6 +47,7 @@ import {
} from '@/lib/workspace' } from '@/lib/workspace'
import { createProject, listProjects, upsertProject } from '@/lib/projectRegistry' import { createProject, listProjects, upsertProject } from '@/lib/projectRegistry'
import { createProjectKvAdapter } from '@/lib/projectKvStore' import { createProjectKvAdapter } from '@/lib/projectKvStore'
import { collectActiveProjectSessionLocks } from '@/lib/projectSessionLock'
interface QuickProjectInfoState { interface QuickProjectInfoState {
projectIndustry?: string projectIndustry?: string
@ -75,7 +76,6 @@ 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_INIT_CHANGED_EVENT = 'xm-project-init-changed'
const getActiveProjectId = () => readCurrentProjectId() const getActiveProjectId = () => readCurrentProjectId()
const tabStore = useTabStore() const tabStore = useTabStore()
@ -96,6 +96,8 @@ const existingProjectDialogOpen = ref(false)
const existingProjects = ref<Array<{ id: string; name: string; updatedAt: string }>>([]) const existingProjects = ref<Array<{ id: string; name: string; updatedAt: string }>>([])
const existingProjectLoading = ref(false) const existingProjectLoading = ref(false)
const hasExistingProjects = ref(false) const hasExistingProjects = ref(false)
const openedProjectIds = ref<string[]>([])
let existingProjectPollTimer: ReturnType<typeof setInterval> | null = null
const projectIndustryLabel = computed(() => { const projectIndustryLabel = computed(() => {
const target = String(projectIndustry.value || '').trim() const target = String(projectIndustry.value || '').trim()
if (!target) return '' if (!target) return ''
@ -175,8 +177,19 @@ const openProjectCalc = async () => {
projectDialogOpen.value = true projectDialogOpen.value = true
} }
const refreshExistingProjects = async () => { const syncExistingProjectOpenedState = (projectIds: string[]) => {
openedProjectIds.value = Array.from(collectActiveProjectSessionLocks(projectIds))
}
const isExistingProjectOpened = (projectIdRaw: string) => {
const projectId = String(projectIdRaw || '').trim()
return projectId ? openedProjectIds.value.includes(projectId) : false
}
const refreshExistingProjects = async (options?: { showLoading?: boolean }) => {
if (options?.showLoading !== false) {
existingProjectLoading.value = true existingProjectLoading.value = true
}
try { try {
const projects = listProjects() const projects = listProjects()
.filter(item => item.id !== QUICK_PROJECT_ID) .filter(item => item.id !== QUICK_PROJECT_ID)
@ -191,23 +204,42 @@ const refreshExistingProjects = async () => {
updatedAt: project.updatedAt updatedAt: project.updatedAt
})) }))
hasExistingProjects.value = projects.length > 0 hasExistingProjects.value = projects.length > 0
syncExistingProjectOpenedState(projects.map(project => project.id))
} finally { } finally {
if (options?.showLoading !== false) {
existingProjectLoading.value = false existingProjectLoading.value = false
} }
}
}
const stopExistingProjectPolling = () => {
if (existingProjectPollTimer == null) return
clearInterval(existingProjectPollTimer)
existingProjectPollTimer = null
}
const startExistingProjectPolling = () => {
stopExistingProjectPolling()
existingProjectPollTimer = setInterval(() => {
if (!existingProjectDialogOpen.value) return
void refreshExistingProjects({ showLoading: false })
}, 3000)
} }
const openExistingProjectDialog = async () => { const openExistingProjectDialog = async () => {
existingProjectDialogOpen.value = true existingProjectDialogOpen.value = true
await refreshExistingProjects() await refreshExistingProjects()
startExistingProjectPolling()
} }
const closeExistingProjectDialog = () => { const closeExistingProjectDialog = () => {
existingProjectDialogOpen.value = false existingProjectDialogOpen.value = false
stopExistingProjectPolling()
} }
const enterExistingProject = (projectIdRaw: string) => { const enterExistingProject = (projectIdRaw: string) => {
const projectId = String(projectIdRaw || '').trim() const projectId = String(projectIdRaw || '').trim()
if (!projectId) return if (!projectId || isExistingProjectOpened(projectId)) return
upsertProject(projectId, resolveProjectRegistryName(projectId)) upsertProject(projectId, resolveProjectRegistryName(projectId))
if (!navigateToWorkspace(projectId, 'project')) return if (!navigateToWorkspace(projectId, 'project')) return
tabStore.enterWorkspace({ tabStore.enterWorkspace({
@ -229,8 +261,6 @@ const confirmProjectCalc = async () => {
projectSubmitting.value = true projectSubmitting.value = true
try { try {
const activeProjectId = getActiveProjectId()
if (activeProjectId === DEFAULT_PROJECT_ID) {
const project = createProject(t('xmInfo.defaultProjectName')) const project = createProject(t('xmInfo.defaultProjectName'))
const kvAdapter = createProjectKvAdapter(project.id) const kvAdapter = createProjectKvAdapter(project.id)
await kvAdapter.setItem<ProjectInfoState>(PROJECT_INFO_KEY, { await kvAdapter.setItem<ProjectInfoState>(PROJECT_INFO_KEY, {
@ -249,24 +279,6 @@ const confirmProjectCalc = async () => {
) )
writeWorkspaceMode('project') writeWorkspaceMode('project')
window.location.href = buildProjectUrl(project.id, { forceHome: false, newProject: false }) window.location.href = buildProjectUrl(project.id, { forceHome: false, newProject: false })
return
}
await kvStore.setItem<ProjectInfoState>(PROJECT_INFO_KEY, {
projectIndustry: industry,
projectName: t('xmInfo.defaultProjectName'),
preparedBy: '',
reviewedBy: '',
preparedCompany: '',
preparedDate: getTodayDateString()
})
await initializeProjectFactorStates(
kvStore,
industry,
PROJECT_CONSULT_CATEGORY_FACTOR_KEY,
PROJECT_MAJOR_FACTOR_KEY
)
window.dispatchEvent(new CustomEvent<boolean>(PROJECT_INIT_CHANGED_EVENT, { detail: true }))
enterProjectCalc()
} finally { } finally {
projectSubmitting.value = false projectSubmitting.value = false
projectDialogOpen.value = false projectDialogOpen.value = false
@ -369,10 +381,22 @@ const confirmHomeImport = async () => {
cancelHomeImportConfirm() cancelHomeImportConfirm()
} }
const handleHomeWindowFocus = () => {
if (!existingProjectDialogOpen.value) return
void refreshExistingProjects({ showLoading: false })
}
const handleHomeVisibilityChange = () => {
if (document.visibilityState !== 'visible') return
handleHomeWindowFocus()
}
onMounted(() => { onMounted(() => {
void refreshExistingProjects() void refreshExistingProjects()
void loadProjectDefaults() void loadProjectDefaults()
void loadQuickDefaults() void loadQuickDefaults()
window.addEventListener('focus', handleHomeWindowFocus)
document.addEventListener('visibilitychange', handleHomeVisibilityChange)
try { try {
const url = new URL(window.location.href) const url = new URL(window.location.href)
const isNewProject = url.searchParams.get(NEW_PROJECT_QUERY_KEY) === '1' const isNewProject = url.searchParams.get(NEW_PROJECT_QUERY_KEY) === '1'
@ -392,6 +416,12 @@ onMounted(() => {
// ignore url parsing errors // ignore url parsing errors
} }
}) })
onBeforeUnmount(() => {
stopExistingProjectPolling()
window.removeEventListener('focus', handleHomeWindowFocus)
document.removeEventListener('visibilitychange', handleHomeVisibilityChange)
})
</script> </script>
<template> <template>
@ -580,11 +610,20 @@ onMounted(() => {
v-for="project in existingProjects" v-for="project in existingProjects"
:key="project.id" :key="project.id"
type="button" type="button"
class="flex w-full cursor-pointer items-center justify-between rounded-lg border border-slate-200 px-3 py-2 text-left transition hover:border-slate-300 hover:bg-slate-50" :disabled="isExistingProjectOpened(project.id)"
class="flex w-full items-center justify-between rounded-lg border border-slate-200 px-3 py-2 text-left transition"
:class="isExistingProjectOpened(project.id)
? 'cursor-not-allowed border-slate-200 bg-slate-100/80 opacity-70'
: 'cursor-pointer hover:border-slate-300 hover:bg-slate-50'"
@click="enterExistingProject(project.id)" @click="enterExistingProject(project.id)"
> >
<div class="min-w-0"> <div class="min-w-0">
<div class="truncate text-sm font-medium text-slate-800">{{ project.name }}</div> <div class="truncate text-sm font-medium text-slate-800">
{{ project.name }}
<span v-if="isExistingProjectOpened(project.id)" class="ml-1 text-xs text-slate-500">
{{ t('tab.toolbar.opened') }}
</span>
</div>
<div class="mt-0.5 text-xs text-slate-500">{{ project.id }}</div> <div class="mt-0.5 text-xs text-slate-500">{{ project.id }}</div>
</div> </div>
<div class="shrink-0 pl-2 text-xs text-slate-500"> <div class="shrink-0 pl-2 text-xs text-slate-500">

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onActivated, onMounted, ref, watch } from 'vue' import { computed, onActivated, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Check, ChevronDown, Circle, CircleDot } from 'lucide-vue-next' import { Circle, CircleDot } from 'lucide-vue-next'
import { import {
getQuickCalcGroups, getQuickCalcGroups,
getMajorDictItemById, getMajorDictItemById,
@ -17,18 +17,6 @@ import { getIndustryMajorEntry } from '@/lib/pricingScaleCalc'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee' import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
import { QUICK_PROJECT_INFO_KEY } from '@/lib/workspace' import { QUICK_PROJECT_INFO_KEY } from '@/lib/workspace'
import { initializeProjectFactorStates } from '@/lib/projectWorkspace' import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
import {
SelectContent,
SelectIcon,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectPortal,
SelectRoot,
SelectTrigger,
SelectValue,
SelectViewport
} from 'reka-ui'
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string
@ -140,6 +128,15 @@ const industryLabel = computed(() => {
return getIndustryDisplayName(target, locale.value) || t('quickCalc.notSelected') return getIndustryDisplayName(target, locale.value) || t('quickCalc.notSelected')
}) })
const isIndustrySelected = (industryId: string | number) => {
return projectIndustry.value.trim() === String(industryId).trim()
}
const handleIndustrySelect = (industryId: string | number) => {
const nextIndustry = String(industryId).trim()
projectIndustry.value = isIndustrySelected(nextIndustry) ? '' : nextIndustry
}
const selectedConsultOption = computed(() => const selectedConsultOption = computed(() =>
quickCalcGroups quickCalcGroups
.find(item => item.key === 'consult') .find(item => item.key === 'consult')
@ -446,41 +443,44 @@ watch(canUseLandScale, enabled => {
<div class="quick-calc-toolbar"> <div class="quick-calc-toolbar">
<label class="quick-calc-toolbar__field"> <label class="quick-calc-toolbar__field quick-calc-toolbar__field--cards">
<span class="quick-calc-field__label">{{ t('quickCalc.fields.industry') }}</span> <span class="quick-calc-field__label">{{ t('quickCalc.fields.industry') }}</span>
<SelectRoot v-model="projectIndustry"> <div class="quick-calc-industry-grid" role="radiogroup" :aria-label="t('quickCalc.fields.industry')">
<SelectTrigger class="quick-calc-toolbar__trigger"> <label
<SelectValue :placeholder="t('quickCalc.selectIndustry')" />
<SelectIcon as-child>
<ChevronDown class="h-4 w-4 text-[var(--qc-muted)]" />
</SelectIcon>
</SelectTrigger>
<SelectPortal>
<SelectContent
:side-offset="6"
position="popper"
class="z-[120] w-[var(--reka-select-trigger-width)] overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-xl"
>
<SelectViewport class="p-1">
<SelectItem
v-for="item in industryTypeList" v-for="item in industryTypeList"
:key="`quick-workbench-${item.id}`" :key="`quick-workbench-${item.id}`"
:value="String(item.id)" class="quick-calc-industry-card"
class="relative flex h-9 w-full cursor-default select-none items-center rounded-md pl-3 pr-8 text-sm outline-none data-[highlighted]:bg-muted data-[highlighted]:text-foreground data-[state=checked]:bg-slate-100" :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)"
> >
<SelectItemText>{{ getIndustryDisplayName(item.id, locale) }}</SelectItemText> <input
<SelectItemIndicator class="absolute right-2 inline-flex items-center text-slate-700"> :checked="isIndustrySelected(item.id)"
<Check class="h-4 w-4" /> type="radio"
</SelectItemIndicator> name="quick-calc-industry-choice"
</SelectItem> class="quick-calc-option__input"
</SelectViewport> tabindex="-1"
</SelectContent> >
</SelectPortal> <span
</SelectRoot> 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>
</label> </label>
<span class="quick-calc-toolbar__meta"> <span class="quick-calc-toolbar__meta">
{{ industrySaving ? t('quickCalc.saving') : hasSelectedIndustry ? t('quickCalc.synced') : t('quickCalc.notSelectedIndustry') }} {{ industrySaving ? t('quickCalc.saving') : industryLabel }}
</span> </span>
</div> </div>
@ -739,30 +739,76 @@ watch(canUseLandScale, enabled => {
flex: 1; flex: 1;
} }
.quick-calc-toolbar__trigger { .quick-calc-toolbar__field--cards {
display: inline-flex; align-content: start;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 42px;
padding: 0 14px;
border: 1px solid var(--qc-border);
border-radius: 12px;
background: color-mix(in srgb, white 55%, var(--qc-surface));
color: var(--qc-text);
box-shadow: inset 0 1px 0 color-mix(in srgb, white 80%, transparent);
outline: none;
} }
.quick-calc-toolbar__trigger:focus-visible { .quick-calc-industry-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(136px, 1fr));
gap: 8px;
}
.quick-calc-industry-card {
position: relative;
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
min-height: 42px;
padding: 0 12px;
border: 1px solid var(--qc-border);
border-radius: 12px;
background: color-mix(in srgb, var(--background) 92%, var(--card));
color: var(--qc-text);
cursor: pointer;
transition: transform 160ms ease, border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
}
.quick-calc-industry-card:hover {
transform: translateY(-1px);
border-color: var(--qc-border-strong); border-color: var(--qc-border-strong);
box-shadow: 0 0 0 3px color-mix(in srgb, hsl(var(--destructive)) 14%, transparent); }
.quick-calc-industry-card:focus-visible {
outline: none;
border-color: color-mix(in srgb, hsl(var(--destructive)) 42%, var(--qc-border));
box-shadow: 0 0 0 3px color-mix(in srgb, hsl(var(--destructive)) 10%, transparent);
}
.quick-calc-industry-card.is-selected {
border-color: color-mix(in srgb, hsl(var(--destructive)) 42%, var(--qc-border));
background: color-mix(in srgb, hsl(var(--destructive)) 7%, white);
box-shadow: 0 0 0 3px color-mix(in srgb, hsl(var(--destructive)) 10%, transparent);
}
.quick-calc-industry-card__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
flex: 0 0 14px;
color: color-mix(in srgb, var(--foreground) 44%, transparent);
transition: color 140ms ease, transform 140ms ease;
}
.quick-calc-industry-card__icon.is-selected {
color: hsl(var(--destructive));
transform: scale(1.02);
}
.quick-calc-industry-card__text {
min-width: 0;
font-size: 14px;
line-height: 1.3;
} }
.quick-calc-toolbar__meta { .quick-calc-toolbar__meta {
flex-shrink: 0; flex-shrink: 0;
font-size: 12px; font-size: 12px;
color: var(--qc-muted); color: var(--qc-muted);
text-align: right;
} }
.quick-calc-empty-state { .quick-calc-empty-state {
@ -1100,22 +1146,23 @@ watch(canUseLandScale, enabled => {
} }
.quick-calc-panel--form .quick-calc-panel__title { .quick-calc-panel--form .quick-calc-panel__title {
font-size: clamp(0.88rem, 0.94vw, 1rem); font-size: 14px;
} }
.quick-calc-panel--form .quick-calc-panel__eyebrow { .quick-calc-panel--form .quick-calc-panel__eyebrow {
font-size: 9px; font-size: 14px;
} }
.quick-calc-panel--form .quick-calc-status__item { .quick-calc-panel--form .quick-calc-status__item {
min-height: 20px; min-height: 20px;
padding: 0 6px; padding: 0 6px;
font-size: 9px; font-size: 14px;
} }
.quick-calc-panel--form .quick-calc-form { .quick-calc-panel--form .quick-calc-form {
overflow: auto; overflow: auto;
padding: 8px; padding: 8px;
font-size: 14px;
} }
.quick-calc-panel--form .quick-calc-form-stack { .quick-calc-panel--form .quick-calc-form-stack {
@ -1166,11 +1213,13 @@ watch(canUseLandScale, enabled => {
} }
.quick-calc-panel--form .quick-calc-form-section__eyebrow { .quick-calc-panel--form .quick-calc-form-section__eyebrow {
font-size: 9px; font-size: 14px;
letter-spacing: 0;
text-transform: none;
} }
.quick-calc-panel--form .quick-calc-form-section__title { .quick-calc-panel--form .quick-calc-form-section__title {
font-size: 12px; font-size: 14px;
} }
.quick-calc-form-grid { .quick-calc-form-grid {
@ -1222,14 +1271,17 @@ watch(canUseLandScale, enabled => {
} }
.quick-calc-panel--form .quick-calc-field__label { .quick-calc-panel--form .quick-calc-field__label {
font-size: 9px; font-size: 14px;
letter-spacing: 0;
text-transform: none;
font-weight: 600;
} }
.quick-calc-panel--form .quick-calc-field__input, .quick-calc-panel--form .quick-calc-field__input,
.quick-calc-panel--form .quick-calc-field__readonly { .quick-calc-panel--form .quick-calc-field__readonly {
min-height: 32px; min-height: 32px;
padding: 0 8px; padding: 0 8px;
font-size: 12px; font-size: 14px;
border-radius: 9px; border-radius: 9px;
} }
@ -1288,8 +1340,8 @@ watch(canUseLandScale, enabled => {
} }
.quick-calc-panel--form .quick-calc-form-hint { .quick-calc-panel--form .quick-calc-form-hint {
font-size: 9px; font-size: 14px;
line-height: 1.15; line-height: 1.35;
} }
.quick-calc-segment { .quick-calc-segment {
@ -1325,6 +1377,15 @@ watch(canUseLandScale, enabled => {
padding: 8px; padding: 8px;
} }
.quick-calc-toolbar {
align-items: stretch;
flex-direction: column;
}
.quick-calc-toolbar__meta {
text-align: left;
}
.quick-calc-layout { .quick-calc-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@ -67,8 +67,8 @@ export const zhCN = {
projectList: '项目列表', projectList: '项目列表',
projectCount: '项目数量:{count}', projectCount: '项目数量:{count}',
createProject: '新建项目', createProject: '新建项目',
backHome: '返回首页', backHome: '返回入口',
resetAll: '重置全部项目', resetAll: '清除全部项目',
opened: '(已打开)', opened: '(已打开)',
lastEdited: '最后编辑:{time}' lastEdited: '最后编辑:{time}'
}, },

View File

@ -1169,11 +1169,7 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows) const projectScale = projectScaleSource.roughCalcEnabled ? [] : toExportScaleRows(projectScaleSource.detailRows)
const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumNumbers(projectScale.map(item => item.cost)) const projectScaleCost = toFiniteNumber(projectScaleSource.totalAmount) ?? sumNumbers(projectScale.map(item => item.cost))
projectScale.push({
majorid: -1,
major: -1, cost: projectScaleCost,
area: null
})
const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorState.resolved?.detailRows) const projectServiceCoes = buildProjectServiceCoes(consultCategoryFactorState.resolved?.detailRows)
const projectMajorCoes = buildProjectMajorCoes(majorFactorState.resolved?.detailRows) const projectMajorCoes = buildProjectMajorCoes(majorFactorState.resolved?.detailRows)
@ -1374,11 +1370,7 @@ const buildExportReportPayload = async (): Promise<ExportReportPayload> => {
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 contractScale = htInfoRaw?.roughCalcEnabled ? [] : toExportScaleRows(htInfoRaw?.detailRows)
contractScale.push({
majorid: -1,
major: -1, cost: contractFee,
area: null
})
const contractServiceCoesRaw = buildProjectServiceCoes(htConsultCategoryFactorState.resolved?.detailRows) const contractServiceCoesRaw = buildProjectServiceCoes(htConsultCategoryFactorState.resolved?.detailRows)
const contractMajorCoesRaw = buildProjectMajorCoes(htMajorFactorState.resolved?.detailRows) const contractMajorCoesRaw = buildProjectMajorCoes(htMajorFactorState.resolved?.detailRows)
console.log('[export][contract factor rows]', { console.log('[export][contract factor rows]', {