完成大部分
This commit is contained in:
1 parent
e97707ac59
commit
13b03e016e
21 files changed
+1628
-275
No files matched your search
@@ -5,8 +5,8 @@
|
||||
} from "ag-grid-community"
|
||||
const borderConfig = {
|
||||
style: "solid", // 虚线改实线更简洁,也可保留 dotted 但建议用 solid
|
||||
width: 0.5, // 更细的边框,减少视觉干扰
|
||||
color: "#e5e7eb" // 浅灰色边框,清新不刺眼
|
||||
width: 0.3, // 更细的边框,减少视觉干扰
|
||||
color: "#d3d3d3" // 浅灰色边框,清新不刺眼
|
||||
};
|
||||
|
||||
// 简洁清新风格的主题配置
|
||||
@@ -32,10 +32,15 @@ export const myTheme = themeQuartz.withParams({
|
||||
export const gridOptions: GridOptions<any> = {
|
||||
treeData: true,
|
||||
animateRows: true,
|
||||
tooltipShowMode: 'whenTruncated',
|
||||
suppressAggFuncInHeader: true,
|
||||
singleClickEdit: true,
|
||||
suppressClickEdit: false,
|
||||
suppressContextMenu: false,
|
||||
autoSizeStrategy: {
|
||||
type: 'fitGridWidth',
|
||||
defaultMinWidth: 100,
|
||||
},
|
||||
groupDefaultExpanded: -1,
|
||||
suppressFieldDotNotation: true,
|
||||
getDataPath: data => data.path,
|
||||
@@ -43,6 +48,12 @@ export const gridOptions: GridOptions<any> = {
|
||||
defaultColDef: {
|
||||
resizable: true,
|
||||
sortable: false,
|
||||
filter: false
|
||||
filter: false,
|
||||
wrapHeaderText: true,
|
||||
autoHeaderHeight: true
|
||||
},
|
||||
defaultColGroupDef: {
|
||||
wrapHeaderText: true,
|
||||
autoHeaderHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export const formatThousands = (value: unknown, fractionDigits = 2) => {
|
||||
if (value === '' || value == null) return ''
|
||||
const numericValue = Number(value)
|
||||
if (!Number.isFinite(numericValue)) return ''
|
||||
return numericValue.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: fractionDigits,
|
||||
maximumFractionDigits: fractionDigits
|
||||
})
|
||||
}
|
||||
|
||||
export const formatThousandsFlexible = (value: unknown, maxFractionDigits = 20) => {
|
||||
if (value === '' || value == null) return ''
|
||||
const numericValue = Number(value)
|
||||
if (!Number.isFinite(numericValue)) return ''
|
||||
return numericValue.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: maxFractionDigits
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
import localforage from 'localforage'
|
||||
import { expertList, getBasicFeeFromScale, majorList, serviceList, taskList } from '@/sql'
|
||||
import { addNumbers, roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
|
||||
|
||||
interface StoredDetailRowsState<T = any> {
|
||||
detailRows?: T[]
|
||||
}
|
||||
|
||||
type MaybeNumber = number | null | undefined
|
||||
|
||||
interface ScaleRow {
|
||||
id: string
|
||||
amount: number | null
|
||||
landArea: number | null
|
||||
consultCategoryFactor: number | null
|
||||
majorFactor: number | null
|
||||
}
|
||||
|
||||
interface WorkloadRow {
|
||||
id: string
|
||||
conversion: number | null
|
||||
workload: number | null
|
||||
budgetAdoptedUnitPrice: number | null
|
||||
consultCategoryFactor: number | null
|
||||
}
|
||||
|
||||
interface HourlyRow {
|
||||
id: string
|
||||
adoptedBudgetUnitPrice: number | null
|
||||
personnelCount: number | null
|
||||
workdayCount: number | null
|
||||
}
|
||||
|
||||
interface MajorLite {
|
||||
code: string
|
||||
defCoe: number | null
|
||||
}
|
||||
|
||||
interface ServiceLite {
|
||||
defCoe: number | null
|
||||
}
|
||||
|
||||
interface TaskLite {
|
||||
serviceID: number
|
||||
conversion: number | null
|
||||
defPrice: number | null
|
||||
}
|
||||
|
||||
interface ExpertLite {
|
||||
defPrice: number | null
|
||||
manageCoe: number | null
|
||||
}
|
||||
|
||||
export interface PricingMethodTotals {
|
||||
investScale: number | null
|
||||
landScale: number | null
|
||||
workload: number | null
|
||||
hourly: number | null
|
||||
}
|
||||
|
||||
const toFiniteNumberOrNull = (value: unknown): number | null =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : null
|
||||
|
||||
const hasOwn = (obj: unknown, key: string) =>
|
||||
Object.prototype.hasOwnProperty.call(obj || {}, key)
|
||||
|
||||
const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
|
||||
const service = (serviceList as Record<string, ServiceLite | undefined>)[String(serviceId)]
|
||||
return toFiniteNumberOrNull(service?.defCoe)
|
||||
}
|
||||
|
||||
const getDefaultMajorFactorById = (id: string) => {
|
||||
const major = (majorList as Record<string, MajorLite | undefined>)[id]
|
||||
return toFiniteNumberOrNull(major?.defCoe)
|
||||
}
|
||||
|
||||
const getMajorLeafIds = () =>
|
||||
Object.entries(majorList as Record<string, MajorLite>)
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
.filter(([, item]) => Boolean(item?.code && item.code.includes('-')))
|
||||
.map(([id]) => id)
|
||||
|
||||
const buildDefaultScaleRows = (serviceId: string | number): ScaleRow[] => {
|
||||
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
|
||||
return getMajorLeafIds().map(id => ({
|
||||
id,
|
||||
amount: null,
|
||||
landArea: null,
|
||||
consultCategoryFactor: defaultConsultCategoryFactor,
|
||||
majorFactor: getDefaultMajorFactorById(id)
|
||||
}))
|
||||
}
|
||||
|
||||
const mergeScaleRows = (
|
||||
serviceId: string | number,
|
||||
rowsFromDb: Array<Partial<ScaleRow> & Pick<ScaleRow, 'id'>> | undefined
|
||||
): ScaleRow[] => {
|
||||
const dbValueMap = new Map<string, Partial<ScaleRow> & Pick<ScaleRow, 'id'>>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
dbValueMap.set(String(row.id), row)
|
||||
}
|
||||
|
||||
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
|
||||
return buildDefaultScaleRows(serviceId).map(row => {
|
||||
const fromDb = dbValueMap.get(row.id)
|
||||
if (!fromDb) return row
|
||||
|
||||
const hasConsultCategoryFactor = hasOwn(fromDb, 'consultCategoryFactor')
|
||||
const hasMajorFactor = hasOwn(fromDb, 'majorFactor')
|
||||
|
||||
return {
|
||||
...row,
|
||||
amount: toFiniteNumberOrNull(fromDb.amount),
|
||||
landArea: toFiniteNumberOrNull(fromDb.landArea),
|
||||
consultCategoryFactor:
|
||||
toFiniteNumberOrNull(fromDb.consultCategoryFactor) ??
|
||||
(hasConsultCategoryFactor ? null : defaultConsultCategoryFactor),
|
||||
majorFactor:
|
||||
toFiniteNumberOrNull(fromDb.majorFactor) ??
|
||||
(hasMajorFactor ? null : getDefaultMajorFactorById(row.id))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getBenchmarkBudgetByAmount = (amount: MaybeNumber) => {
|
||||
const result = getBasicFeeFromScale(amount, 'cost')
|
||||
return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
|
||||
}
|
||||
|
||||
const getBenchmarkBudgetByLandArea = (landArea: MaybeNumber) => {
|
||||
const result = getBasicFeeFromScale(landArea, 'area')
|
||||
return result ? roundTo(addNumbers(result.basic, result.optional), 2) : null
|
||||
}
|
||||
|
||||
const getInvestmentBudgetFee = (row: ScaleRow) => {
|
||||
const benchmarkBudget = getBenchmarkBudgetByAmount(row.amount)
|
||||
if (benchmarkBudget == null || row.majorFactor == null || row.consultCategoryFactor == null) return null
|
||||
return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2)
|
||||
}
|
||||
|
||||
const getLandBudgetFee = (row: ScaleRow) => {
|
||||
const benchmarkBudget = getBenchmarkBudgetByLandArea(row.landArea)
|
||||
if (benchmarkBudget == null || row.majorFactor == null || row.consultCategoryFactor == null) return null
|
||||
return roundTo(toDecimal(benchmarkBudget).mul(row.majorFactor).mul(row.consultCategoryFactor), 2)
|
||||
}
|
||||
|
||||
const getTaskEntriesByServiceId = (serviceId: string | number) =>
|
||||
Object.entries(taskList as Record<string, TaskLite>)
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
.filter(([, task]) => Number(task.serviceID) === Number(serviceId))
|
||||
|
||||
const buildDefaultWorkloadRows = (serviceId: string | number): WorkloadRow[] => {
|
||||
const defaultConsultCategoryFactor = getDefaultConsultCategoryFactor(serviceId)
|
||||
return getTaskEntriesByServiceId(serviceId).map(([taskId, task], order) => ({
|
||||
id: `task-${taskId}-${order}`,
|
||||
conversion: toFiniteNumberOrNull(task.conversion),
|
||||
workload: null,
|
||||
budgetAdoptedUnitPrice: toFiniteNumberOrNull(task.defPrice),
|
||||
consultCategoryFactor: defaultConsultCategoryFactor
|
||||
}))
|
||||
}
|
||||
|
||||
const mergeWorkloadRows = (
|
||||
serviceId: string | number,
|
||||
rowsFromDb: Array<Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>> | undefined
|
||||
): WorkloadRow[] => {
|
||||
const dbValueMap = new Map<string, Partial<WorkloadRow> & Pick<WorkloadRow, 'id'>>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
dbValueMap.set(String(row.id), row)
|
||||
}
|
||||
|
||||
return buildDefaultWorkloadRows(serviceId).map(row => {
|
||||
const fromDb = dbValueMap.get(row.id)
|
||||
if (!fromDb) return row
|
||||
|
||||
return {
|
||||
...row,
|
||||
workload: toFiniteNumberOrNull(fromDb.workload),
|
||||
budgetAdoptedUnitPrice: toFiniteNumberOrNull(fromDb.budgetAdoptedUnitPrice),
|
||||
consultCategoryFactor: toFiniteNumberOrNull(fromDb.consultCategoryFactor)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const calcWorkloadServiceFee = (row: WorkloadRow) => {
|
||||
if (
|
||||
row.budgetAdoptedUnitPrice == null ||
|
||||
row.conversion == null ||
|
||||
row.workload == null ||
|
||||
row.consultCategoryFactor == null
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return roundTo(
|
||||
toDecimal(row.budgetAdoptedUnitPrice).mul(row.conversion).mul(row.workload).mul(row.consultCategoryFactor),
|
||||
2
|
||||
)
|
||||
}
|
||||
|
||||
const getExpertEntries = () =>
|
||||
Object.entries(expertList as Record<string, ExpertLite>).sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
|
||||
const getDefaultHourlyAdoptedPrice = (expert: ExpertLite) => {
|
||||
if (expert.defPrice == null || expert.manageCoe == null) return null
|
||||
return roundTo(toDecimal(expert.defPrice).mul(expert.manageCoe), 2)
|
||||
}
|
||||
|
||||
const buildDefaultHourlyRows = (): HourlyRow[] =>
|
||||
getExpertEntries().map(([expertId, expert]) => ({
|
||||
id: `expert-${expertId}`,
|
||||
adoptedBudgetUnitPrice: getDefaultHourlyAdoptedPrice(expert),
|
||||
personnelCount: null,
|
||||
workdayCount: null
|
||||
}))
|
||||
|
||||
const mergeHourlyRows = (
|
||||
rowsFromDb: Array<Partial<HourlyRow> & Pick<HourlyRow, 'id'>> | undefined
|
||||
): HourlyRow[] => {
|
||||
const dbValueMap = new Map<string, Partial<HourlyRow> & Pick<HourlyRow, 'id'>>()
|
||||
for (const row of rowsFromDb || []) {
|
||||
dbValueMap.set(String(row.id), row)
|
||||
}
|
||||
|
||||
return buildDefaultHourlyRows().map(row => {
|
||||
const fromDb = dbValueMap.get(row.id)
|
||||
if (!fromDb) return row
|
||||
|
||||
return {
|
||||
...row,
|
||||
adoptedBudgetUnitPrice: toFiniteNumberOrNull(fromDb.adoptedBudgetUnitPrice),
|
||||
personnelCount: toFiniteNumberOrNull(fromDb.personnelCount),
|
||||
workdayCount: toFiniteNumberOrNull(fromDb.workdayCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const calcHourlyServiceBudget = (row: HourlyRow) => {
|
||||
if (row.adoptedBudgetUnitPrice == null || row.personnelCount == null || row.workdayCount == null) return null
|
||||
return roundTo(toDecimal(row.adoptedBudgetUnitPrice).mul(row.personnelCount).mul(row.workdayCount), 2)
|
||||
}
|
||||
|
||||
export const getPricingMethodTotalsForService = async (params: {
|
||||
contractId: string
|
||||
serviceId: string | number
|
||||
}): Promise<PricingMethodTotals> => {
|
||||
const serviceId = String(params.serviceId)
|
||||
const htDbKey = `ht-info-v3-${params.contractId}`
|
||||
const investDbKey = `tzGMF-${params.contractId}-${serviceId}`
|
||||
const landDbKey = `ydGMF-${params.contractId}-${serviceId}`
|
||||
const workloadDbKey = `gzlF-${params.contractId}-${serviceId}`
|
||||
const hourlyDbKey = `hourlyPricing-${params.contractId}-${serviceId}`
|
||||
|
||||
const [investData, landData, workloadData, hourlyData, htData] = await Promise.all([
|
||||
localforage.getItem<StoredDetailRowsState>(investDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(landDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(workloadDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(hourlyDbKey),
|
||||
localforage.getItem<StoredDetailRowsState>(htDbKey)
|
||||
])
|
||||
|
||||
const investRows =
|
||||
investData?.detailRows != null
|
||||
? mergeScaleRows(serviceId, investData.detailRows as any)
|
||||
: htData?.detailRows != null
|
||||
? mergeScaleRows(serviceId, htData.detailRows as any)
|
||||
: buildDefaultScaleRows(serviceId)
|
||||
const investScale = sumByNumber(investRows, row => getInvestmentBudgetFee(row))
|
||||
|
||||
const landRows =
|
||||
landData?.detailRows != null
|
||||
? mergeScaleRows(serviceId, landData.detailRows as any)
|
||||
: htData?.detailRows != null
|
||||
? mergeScaleRows(serviceId, htData.detailRows as any)
|
||||
: buildDefaultScaleRows(serviceId)
|
||||
const landScale = sumByNumber(landRows, row => getLandBudgetFee(row))
|
||||
|
||||
const defaultWorkloadRows = buildDefaultWorkloadRows(serviceId)
|
||||
const workload =
|
||||
defaultWorkloadRows.length === 0
|
||||
? null
|
||||
: sumByNumber(
|
||||
workloadData?.detailRows != null
|
||||
? mergeWorkloadRows(serviceId, workloadData.detailRows as any)
|
||||
: defaultWorkloadRows,
|
||||
row => calcWorkloadServiceFee(row)
|
||||
)
|
||||
|
||||
const hourlyRows =
|
||||
hourlyData?.detailRows != null
|
||||
? mergeHourlyRows(hourlyData.detailRows as any)
|
||||
: buildDefaultHourlyRows()
|
||||
const hourly = sumByNumber(hourlyRows, row => calcHourlyServiceBudget(row))
|
||||
|
||||
return {
|
||||
investScale,
|
||||
landScale,
|
||||
workload,
|
||||
hourly
|
||||
}
|
||||
}
|
||||
|
||||
export const getPricingMethodTotalsForServices = async (params: {
|
||||
contractId: string
|
||||
serviceIds: Array<string | number>
|
||||
}) => {
|
||||
const result = new Map<string, PricingMethodTotals>()
|
||||
await Promise.all(
|
||||
params.serviceIds.map(async serviceId => {
|
||||
const totals = await getPricingMethodTotalsForService({
|
||||
contractId: params.contractId,
|
||||
serviceId
|
||||
})
|
||||
result.set(String(serviceId), totals)
|
||||
})
|
||||
)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import localforage from 'localforage'
|
||||
|
||||
const CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
|
||||
const MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
|
||||
|
||||
type XmFactorRow = {
|
||||
id: string
|
||||
standardFactor: number | null
|
||||
budgetValue: number | null
|
||||
}
|
||||
|
||||
type XmFactorState = {
|
||||
detailRows?: XmFactorRow[]
|
||||
}
|
||||
|
||||
const toFiniteNumberOrNull = (value: unknown): number | null =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : null
|
||||
|
||||
const resolveFactorValue = (row: Partial<XmFactorRow> | undefined): number | null => {
|
||||
if (!row) return null
|
||||
const budgetValue = toFiniteNumberOrNull(row.budgetValue)
|
||||
if (budgetValue != null) return budgetValue
|
||||
return toFiniteNumberOrNull(row.standardFactor)
|
||||
}
|
||||
|
||||
const loadFactorMap = async (storageKey: string): Promise<Map<string, number | null>> => {
|
||||
const data = await localforage.getItem<XmFactorState>(storageKey)
|
||||
const map = new Map<string, number | null>()
|
||||
for (const row of data?.detailRows || []) {
|
||||
if (!row?.id) continue
|
||||
map.set(String(row.id), resolveFactorValue(row))
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export const loadConsultCategoryFactorMap = async () => loadFactorMap(CONSULT_CATEGORY_FACTOR_KEY)
|
||||
|
||||
export const loadMajorFactorMap = async () => loadFactorMap(MAJOR_FACTOR_KEY)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
const ZW_VERSION = 1
|
||||
const KEY_SEED = 'JGJS2026::ZW::ARCHIVE::V1::DO_NOT_TAMPER'
|
||||
const MAGIC_BYTES = new Uint8Array([0x4a, 0x47, 0x4a, 0x53, 0x5a, 0x57]) // JGJSZW
|
||||
|
||||
export const ZW_FILE_EXTENSION = '.zw'
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
const decoder = new TextDecoder()
|
||||
let cachedKeyPromise: Promise<CryptoKey> | null = null
|
||||
|
||||
const toArrayBuffer = (bytes: Uint8Array): ArrayBuffer =>
|
||||
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
|
||||
|
||||
const getArchiveKey = async (): Promise<CryptoKey> => {
|
||||
if (!cachedKeyPromise) {
|
||||
cachedKeyPromise = (async () => {
|
||||
const hash = await crypto.subtle.digest('SHA-256', encoder.encode(KEY_SEED))
|
||||
return crypto.subtle.importKey('raw', hash, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'])
|
||||
})()
|
||||
}
|
||||
return cachedKeyPromise
|
||||
}
|
||||
|
||||
export const encodeZwArchive = async (payload: unknown): Promise<Uint8Array> => {
|
||||
const key = await getArchiveKey()
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12))
|
||||
const ivBuffer = toArrayBuffer(iv)
|
||||
const plainText = encoder.encode(JSON.stringify(payload))
|
||||
const cipherBuffer = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv: ivBuffer },
|
||||
key,
|
||||
toArrayBuffer(plainText)
|
||||
)
|
||||
const cipher = new Uint8Array(cipherBuffer)
|
||||
const result = new Uint8Array(MAGIC_BYTES.length + 1 + 1 + iv.length + cipher.length)
|
||||
let offset = 0
|
||||
result.set(MAGIC_BYTES, offset)
|
||||
offset += MAGIC_BYTES.length
|
||||
result[offset] = ZW_VERSION
|
||||
offset += 1
|
||||
result[offset] = iv.length
|
||||
offset += 1
|
||||
result.set(iv, offset)
|
||||
offset += iv.length
|
||||
result.set(cipher, offset)
|
||||
return result
|
||||
}
|
||||
|
||||
export const decodeZwArchive = async <T>(raw: ArrayBuffer | Uint8Array): Promise<T> => {
|
||||
const bytes = raw instanceof Uint8Array ? raw : new Uint8Array(raw)
|
||||
const minLength = MAGIC_BYTES.length + 1 + 1 + 1 + 16
|
||||
if (bytes.length < minLength) {
|
||||
throw new Error('INVALID_ZW_PAYLOAD')
|
||||
}
|
||||
|
||||
let offset = 0
|
||||
for (let i = 0; i < MAGIC_BYTES.length; i += 1) {
|
||||
if (bytes[offset + i] !== MAGIC_BYTES[i]) {
|
||||
throw new Error('INVALID_ZW_HEADER')
|
||||
}
|
||||
}
|
||||
offset += MAGIC_BYTES.length
|
||||
|
||||
const version = bytes[offset]
|
||||
offset += 1
|
||||
if (version !== ZW_VERSION) {
|
||||
throw new Error('INVALID_ZW_VERSION')
|
||||
}
|
||||
|
||||
const ivLength = bytes[offset]
|
||||
offset += 1
|
||||
if (ivLength <= 0 || bytes.length <= offset + ivLength) {
|
||||
throw new Error('INVALID_ZW_PAYLOAD')
|
||||
}
|
||||
|
||||
const iv = bytes.slice(offset, offset + ivLength)
|
||||
offset += ivLength
|
||||
const cipher = bytes.slice(offset)
|
||||
if (cipher.length < 16) {
|
||||
throw new Error('INVALID_ZW_PAYLOAD')
|
||||
}
|
||||
|
||||
const key = await getArchiveKey()
|
||||
const ivBuffer = toArrayBuffer(iv)
|
||||
const cipherBuffer = toArrayBuffer(cipher)
|
||||
|
||||
let plainBuffer: ArrayBuffer
|
||||
try {
|
||||
plainBuffer = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: ivBuffer }, key, cipherBuffer)
|
||||
} catch {
|
||||
throw new Error('INVALID_ZW_TAMPERED')
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(decoder.decode(plainBuffer)) as T
|
||||
} catch {
|
||||
throw new Error('INVALID_ZW_CONTENT')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import localforage from 'localforage'
|
||||
|
||||
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly'
|
||||
|
||||
interface ZxFwDetailRow {
|
||||
id: string
|
||||
investScale: number | null
|
||||
landScale: number | null
|
||||
workload: number | null
|
||||
hourly: number | null
|
||||
}
|
||||
|
||||
interface ZxFwState {
|
||||
selectedIds?: string[]
|
||||
selectedCodes?: string[]
|
||||
detailRows: ZxFwDetailRow[]
|
||||
}
|
||||
|
||||
export const ZXFW_RELOAD_SERVICE_KEY = 'zxfw-main'
|
||||
|
||||
const toFiniteNumberOrNull = (value: number | null | undefined) =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : null
|
||||
|
||||
export const syncPricingTotalToZxFw = async (params: {
|
||||
contractId: string
|
||||
serviceId: string | number
|
||||
field: ZxFwPricingField
|
||||
value: number | null | undefined
|
||||
}) => {
|
||||
const dbKey = `zxFW-${params.contractId}`
|
||||
const data = await localforage.getItem<ZxFwState>(dbKey)
|
||||
if (!data?.detailRows?.length) return false
|
||||
|
||||
const targetServiceId = String(params.serviceId)
|
||||
const nextValue = toFiniteNumberOrNull(params.value)
|
||||
let changed = false
|
||||
|
||||
const nextRows = data.detailRows.map(row => {
|
||||
if (String(row.id) !== targetServiceId) return row
|
||||
const currentValue = toFiniteNumberOrNull(row[params.field])
|
||||
if (currentValue === nextValue) return row
|
||||
changed = true
|
||||
return {
|
||||
...row,
|
||||
[params.field]: nextValue
|
||||
}
|
||||
})
|
||||
|
||||
if (!changed) return false
|
||||
|
||||
await localforage.setItem(dbKey, {
|
||||
...data,
|
||||
detailRows: nextRows
|
||||
})
|
||||
return true
|
||||
}
|
||||
Reference in new issue
Block a user