From 39161e43e4d4ee70c096fb6ec095ba9f6e531bec Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Thu, 10 Sep 2026 18:06:52 +0800 Subject: [PATCH] feat(web-admin): plan form package UI keyed by product and package spec Contract-bound stop products use package options from the contract, arrange-qty cache/API include packageSpec, and save validates required package selections. Co-authored-by: Cursor --- web-admin/src/api/pl/index.ts | 3 + .../index.vue | 11 +- .../pl-plan-delivery-section/index.vue | 16 ++- .../business/pl-plan-form-dialog/index.vue | 95 ++++++++++++- .../business/pl-plan-load-section/index.vue | 7 +- .../pl-plan-stop-product-table/index.vue | 129 +++++++++++++++--- .../src/utils/pl-contract-qty-reminder.ts | 7 +- .../src/utils/pl-plan-contract-product.ts | 99 ++++++++++++-- web-admin/src/utils/pl-plan-stop-form.ts | 12 ++ 9 files changed, 342 insertions(+), 37 deletions(-) diff --git a/web-admin/src/api/pl/index.ts b/web-admin/src/api/pl/index.ts index 079ebe383..9d12ce87f 100644 --- a/web-admin/src/api/pl/index.ts +++ b/web-admin/src/api/pl/index.ts @@ -901,6 +901,7 @@ export interface ContractProductArrangeQtyDetail { contractNo?: string | null customerName?: string | null productId: string + packageSpec?: string | null productName?: string | null hasFixedQty: boolean contractQty?: number | null @@ -923,6 +924,7 @@ export const planApi = { getContractProductArrangeQty: (params: { contractId: string productId: string + packageSpec?: string excludePlanId?: string }) => request.get( @@ -1165,6 +1167,7 @@ export interface ContractQtyReminderItem { customerName?: string stopKind: string productId: string + packageSpec?: string | null productName?: string model?: string unit?: string diff --git a/web-admin/src/components/business/pl-contract-product-arrange-qty-dialog/index.vue b/web-admin/src/components/business/pl-contract-product-arrange-qty-dialog/index.vue index 9398d1be4..c411d6660 100644 --- a/web-admin/src/components/business/pl-contract-product-arrange-qty-dialog/index.vue +++ b/web-admin/src/components/business/pl-contract-product-arrange-qty-dialog/index.vue @@ -355,6 +355,7 @@ contractId?: string productId?: string productName?: string + packageSpec?: string excludePlanId?: string }>() @@ -439,6 +440,7 @@ const res = await planApi.getContractProductArrangeQty({ contractId, productId, + packageSpec: props.packageSpec || undefined, excludePlanId: props.excludePlanId || undefined }) const data = pickBxData(res) || null @@ -470,7 +472,14 @@ } watch( - () => [props.modelValue, props.contractId, props.productId, props.excludePlanId] as const, + () => + [ + props.modelValue, + props.contractId, + props.productId, + props.packageSpec, + props.excludePlanId + ] as const, ([visible]) => { if (visible) void loadDetail() } diff --git a/web-admin/src/components/business/pl-plan-delivery-section/index.vue b/web-admin/src/components/business/pl-plan-delivery-section/index.vue index 519f57ae0..ffb440a61 100644 --- a/web-admin/src/components/business/pl-plan-delivery-section/index.vue +++ b/web-admin/src/components/business/pl-plan-delivery-section/index.vue @@ -221,6 +221,7 @@ :allow-remove="!readonly" :contract-id="deliveryContractId(delivery)" :exclude-plan-id="excludePlanId" + :contract-product-rows="contractProductRowsFor(delivery)" @remove="(productIndex) => removeStopProduct(index, productIndex)" /> @@ -245,7 +246,10 @@ type PlanDeliveryForm, type PlanStopProductFormRow } from '@/utils/pl-plan-stop-form' - import { findDeliveryProductsMissingFromLoad } from '@/utils/pl-plan-contract-product' + import { + findDeliveryProductsMissingFromLoad, + type ContractProductRowLite + } from '@/utils/pl-plan-contract-product' import { isPlInternalCustomerName } from '@/utils/pl-internal-customer' import { pickBxData } from '@/utils/http/bx' @@ -263,6 +267,8 @@ loadProducts?: PlanStopProductFormRow[] /** 当前计划车牌(主牌),用于地磅可选车例外 */ vehiclePlate?: string + /** 合同 id → 产品行摘要(包装下拉) */ + contractProductRowsByContract?: Record }>(), { tabMode: true, @@ -272,7 +278,8 @@ showSplitHint: false, excludePlanId: '', loadProducts: () => [], - vehiclePlate: '' + vehiclePlate: '', + contractProductRowsByContract: () => ({}) } ) @@ -297,6 +304,11 @@ return String(delivery.contracts?.[0]?.contractId || '').trim() } + function contractProductRowsFor(delivery: PlanDeliveryForm): ContractProductRowLite[] { + const cid = deliveryContractId(delivery) + return cid ? props.contractProductRowsByContract?.[cid] || [] : [] + } + function selectProductsDisabled(delivery: PlanDeliveryForm) { if (isPlInternalCustomerName(delivery.customerName)) return false return !deliveryContractId(delivery) diff --git a/web-admin/src/components/business/pl-plan-form-dialog/index.vue b/web-admin/src/components/business/pl-plan-form-dialog/index.vue index 0e72fb4e0..86eb3a4ec 100644 --- a/web-admin/src/components/business/pl-plan-form-dialog/index.vue +++ b/web-admin/src/components/business/pl-plan-form-dialog/index.vue @@ -193,6 +193,7 @@ compact-products :exclude-plan-id="planId || ''" :vehicle-plate="currentVehiclePlate" + :contract-product-rows="loadContractProductRows" @open-customer-picker="openCustomerSelector('load')" @add-products="openProductSelector('load')" /> @@ -235,6 +236,7 @@ :exclude-plan-id="planId || ''" :load-products="loadForm.products" :vehicle-plate="currentVehiclePlate" + :contract-product-rows-by-contract="contractProductRowsCache" @active-index-change="deliveryActiveIndex = $event" @open-customer-picker="openCustomerSelector('delivery', $event)" @sync-products="syncDeliveryProductsFromLoadManual" @@ -383,7 +385,8 @@ sumStopProductsPlanQty, syncDeliveriesProductsFromLoad, type PlanDeliveryForm, - type PlanLoadForm + type PlanLoadForm, + type PlanStopProductFormRow } from '@/utils/pl-plan-stop-form' import { findFirstStopProductArrangeGap } from '@/utils/pl-plan-stop-product-arrange-required' import { @@ -418,7 +421,12 @@ import { isPlInternalCustomerName } from '@/utils/pl-internal-customer' import { collectContractProductIds, - filterProductsByAllowedProductIds + collectContractProductRows, + filterProductsByAllowedProductIds, + isPackageAllowedForContractProduct, + normalizePackageSpec, + packageSpecsForProduct, + type ContractProductRowLite } from '@/utils/pl-plan-contract-product' import { getPersonnelAffiliationLabel } from '@/utils/pl-personnel-cert' @@ -555,6 +563,8 @@ let loadContractFetchSeq = 0 /** 合同 id → 明细 productId(装/送货共用缓存) */ const contractProductIdsCache = ref>({}) + /** 合同 id → 明细行摘要(含包装,供段产品表下拉与保存校验) */ + const contractProductRowsCache = ref>({}) /** 送货段合同明细拉取序号(按段) */ const deliveryContractFetchSeqByIndex: Record = {} /** @@ -573,6 +583,11 @@ return loadContractProductIds.value }) + const loadContractProductRows = computed((): ContractProductRowLite[] => { + const cid = String(loadForm.value.contracts?.[0]?.contractId || '').trim() + return cid ? contractProductRowsCache.value[cid] || [] : [] + }) + function openProductSelector(side: 'load' | 'delivery', deliveryIndex = 0) { productSelectorTarget.value = { side, deliveryIndex } productSelectorVisible.value = true @@ -586,12 +601,15 @@ } try { const res = await getContract(id) - const detail = pickBxData<{ products?: Array<{ productId?: string | null }> }>(res) + const detail = pickBxData<{ products?: ContractProductRowLite[] }>(res) + const productRows = collectContractProductRows(detail?.products || []) const allowedIds = collectContractProductIds(detail?.products || []) contractProductIdsCache.value = { ...contractProductIdsCache.value, [id]: allowedIds } + contractProductRowsCache.value = { ...contractProductRowsCache.value, [id]: productRows } return allowedIds } catch { contractProductIdsCache.value = { ...contractProductIdsCache.value, [id]: [] } + contractProductRowsCache.value = { ...contractProductRowsCache.value, [id]: [] } return [] } } @@ -1317,12 +1335,25 @@ async function handleProductConfirm(products: any[]) { const target = productSelectorTarget.value + const resolveDefaultPackageSpec = (productId: string) => { + let rows: ContractProductRowLite[] = [] + if (target.side === 'delivery') { + const delivery = deliveryForms.value[target.deliveryIndex] + const cid = String(delivery?.contracts?.[0]?.contractId || '').trim() + rows = cid ? contractProductRowsCache.value[cid] || [] : [] + } else { + rows = loadContractProductRows.value + } + const specs = packageSpecsForProduct(rows, productId) + return specs.length === 1 ? specs[0] : '' + } const buildRows = async () => { const rows: ReturnType = [] for (const product of products) { const brandMeta = initLineBrandMetaFromProduct(product as Record) + const productId = String(product.id || '') const row: PlanFormProductRow = { - productId: String(product.id || ''), + productId, productNo: String(product.productNo || product.id || ''), productName: String(product.productName || ''), model: product.model as string | undefined, @@ -1330,7 +1361,7 @@ unit: String(product.unit || '吨'), quantity: 1, actualQty: 0, - packageSpec: '', + packageSpec: resolveDefaultPackageSpec(productId), deliveryMethod: '', isToxic: Number(product.isToxic || 0), isExplosive: Number(product.isExplosive || 0), @@ -2009,6 +2040,59 @@ return true } + /** 绑合同时:合同产品有包装选项则包装必填且须在选项内 */ + function validateContractPackageSpecs(): boolean { + const checkProducts = ( + products: PlanStopProductFormRow[] | undefined, + contractId: string, + stopLabel: string + ): boolean => { + const cid = String(contractId || '').trim() + if (!cid) return true + const contractRows = contractProductRowsCache.value[cid] || [] + for (const product of products || []) { + const pid = String(product.productId || '').trim() + if (!pid) continue + const allowed = packageSpecsForProduct(contractRows, pid) + if (!allowed.length) continue + const pkg = normalizePackageSpec(product.packageSpec) + const name = String(product.productName || product.standardProductName || pid).trim() || pid + if (!pkg) { + ElMessage.error(`请选择包装规格:${stopLabel}${name}`) + return false + } + if (!isPackageAllowedForContractProduct(contractRows, pid, pkg)) { + ElMessage.error(`包装规格不在合同范围内:${stopLabel}${name}`) + return false + } + } + return true + } + + if ( + !checkProducts( + loadForm.value.products, + String(loadForm.value.contracts?.[0]?.contractId || ''), + '装货 ' + ) + ) { + return false + } + for (let i = 0; i < deliveryForms.value.length; i += 1) { + const row = deliveryForms.value[i] + if ( + !checkProducts( + row.products, + String(row.contracts?.[0]?.contractId || ''), + `送货段#${i + 1} ` + ) + ) { + return false + } + } + return true + } + /** 非内部客户(山峡/云栋)的装/送货段必须至少选一份合同(保存与安排均校验) */ function validateStopContractsRequired() { if ( @@ -2106,6 +2190,7 @@ const valid = await basicFormRef.value?.validate().catch(() => false) if (!valid || !validateBusinessRules()) return if (!validateSingleContractPerStop()) return + if (!validateContractPackageSpecs()) return if (!validateStopContractsRequired()) return if (mode === 'saveAndArrange' || mode === 'rearrange') { diff --git a/web-admin/src/components/business/pl-plan-load-section/index.vue b/web-admin/src/components/business/pl-plan-load-section/index.vue index ceb91f5f3..f0f3cbf02 100644 --- a/web-admin/src/components/business/pl-plan-load-section/index.vue +++ b/web-admin/src/components/business/pl-plan-load-section/index.vue @@ -150,6 +150,7 @@ :compact="compactProducts" :contract-id="stopContractId" :exclude-plan-id="excludePlanId" + :contract-product-rows="contractProductRows" @remove="removeStopProduct" /> @@ -165,6 +166,7 @@ import PlPlanContractMultiSelect from '@/components/business/pl-plan-contract-multi-select/index.vue' import PlPlanStopProductTable from '@/components/business/pl-plan-stop-product-table/index.vue' import { resolveAddressNotesFill } from '@/utils/pl-address-notes-fill' + import type { ContractProductRowLite } from '@/utils/pl-plan-contract-product' import { canManualToggleStopDibang, resolveStopDibangDefaults, @@ -182,12 +184,15 @@ excludePlanId?: string /** 当前计划车牌(主牌),用于地磅可选车例外 */ vehiclePlate?: string + /** 装货合同产品行摘要(包装下拉) */ + contractProductRows?: ContractProductRowLite[] }>(), { readonly: false, compactProducts: true, excludePlanId: '', - vehiclePlate: '' + vehiclePlate: '', + contractProductRows: () => [] } ) diff --git a/web-admin/src/components/business/pl-plan-stop-product-table/index.vue b/web-admin/src/components/business/pl-plan-stop-product-table/index.vue index d7d781030..e324a538e 100644 --- a/web-admin/src/components/business/pl-plan-stop-product-table/index.vue +++ b/web-admin/src/components/business/pl-plan-stop-product-table/index.vue @@ -45,15 +45,32 @@ 包装规格 + + + {{ - resolveDictLabel('crm_product_gg', row.packageSpec) + resolvePackageDisplay(row) }}
@@ -295,17 +312,39 @@ 包装规格 @@ -453,6 +492,7 @@ :contract-id="arrangeQtyContractId" :product-id="arrangeQtyProductId" :product-name="arrangeQtyProductName" + :package-spec="arrangeQtyPackageSpec" :exclude-plan-id="excludePlanId" />
@@ -467,6 +507,11 @@ resolveStopActualQty, resolveStopUnitPrice } from '@/utils/pl-plan-stop-form' + import { + normalizePackageSpec, + packageSpecsForProduct, + type ContractProductRowLite + } from '@/utils/pl-plan-contract-product' import { getDeliveryMethodLabel } from '@/composables/usePlContractToPlan' import { applyBrandSelectChange, @@ -502,6 +547,8 @@ contractId?: string /** 当前编辑计划 id,计算可安排时排除 */ excludePlanId?: string + /** 本合同产品行摘要(有包装选项时包装列用合同下拉) */ + contractProductRows?: ContractProductRowLite[] }>(), { compact: false, @@ -511,7 +558,8 @@ showStopArchivePrice: false, stopArchiveMode: 'load', contractId: '', - excludePlanId: '' + excludePlanId: '', + contractProductRows: () => [] } ) @@ -540,21 +588,44 @@ const arrangeQtyContractId = ref('') const arrangeQtyProductId = ref('') const arrangeQtyProductName = ref('') + const arrangeQtyPackageSpec = ref('') - function cacheKey(productId: string) { - return `${props.contractId || ''}::${productId}::${props.excludePlanId || ''}` + function packageOptionsFor(row: PlanStopProductFormRow) { + return packageSpecsForProduct(props.contractProductRows || [], String(row.productId || '')) } - async function ensureArrangeQty(productId: string) { + function useContractPackageSelect(row: PlanStopProductFormRow) { + return Boolean(String(props.contractId || '').trim() && packageOptionsFor(row).length > 0) + } + + function resolvePackageDisplay(row: PlanStopProductFormRow) { + const raw = normalizePackageSpec(row.packageSpec) + if (!raw) return '—' + if (useContractPackageSelect(row)) return raw + return resolveDictLabel('crm_product_gg', raw) + } + + function requiresPackageBeforeArrange(row: PlanStopProductFormRow) { + return useContractPackageSelect(row) && !normalizePackageSpec(row.packageSpec) + } + + function cacheKey(productId: string, packageSpec?: string | null) { + return `${props.contractId || ''}::${productId}::${normalizePackageSpec(packageSpec)}::${props.excludePlanId || ''}` + } + + async function ensureArrangeQty(row: PlanStopProductFormRow) { const cid = String(props.contractId || '').trim() - const pid = String(productId || '').trim() + const pid = String(row.productId || '').trim() if (!cid || !pid) return null - const key = cacheKey(pid) + if (requiresPackageBeforeArrange(row)) return null + const pkg = normalizePackageSpec(row.packageSpec) + const key = cacheKey(pid, pkg) if (key in arrangeQtyCache.value) return arrangeQtyCache.value[key] try { const res = await planApi.getContractProductArrangeQty({ contractId: cid, productId: pid, + packageSpec: pkg || undefined, excludePlanId: props.excludePlanId || undefined }) const data = pickBxData(res) || null @@ -566,12 +637,25 @@ } } + function onPackageChange(row: PlanStopProductFormRow) { + const pid = String(row.productId || '').trim() + if (!pid) return + const prefix = `${props.contractId || ''}::${pid}::` + const next: Record = {} + for (const [key, value] of Object.entries(arrangeQtyCache.value)) { + if (!key.startsWith(prefix)) next[key] = value + } + arrangeQtyCache.value = next + void ensureArrangeQty(row) + } + function formatAvailableDisplay(row: PlanStopProductFormRow) { const cid = String(props.contractId || '').trim() if (!cid) return '—' const pid = String(row.productId || '').trim() if (!pid) return '—' - const data = arrangeQtyCache.value[cacheKey(pid)] + if (requiresPackageBeforeArrange(row)) return '请先选包装' + const data = arrangeQtyCache.value[cacheKey(pid, row.packageSpec)] if (!data) return '…' if (!data.hasFixedQty) return '—' if (data.availableQty == null) return '—' @@ -581,8 +665,8 @@ function isPlanQtyOverAvailable(row: PlanStopProductFormRow) { const cid = String(props.contractId || '').trim() const pid = String(row.productId || '').trim() - if (!cid || !pid) return false - const data = arrangeQtyCache.value[cacheKey(pid)] + if (!cid || !pid || requiresPackageBeforeArrange(row)) return false + const data = arrangeQtyCache.value[cacheKey(pid, row.packageSpec)] if (!data?.hasFixedQty || data.availableQty == null) return false const planQty = Number(row.planQty) if (!Number.isFinite(planQty)) return false @@ -590,7 +674,11 @@ } function canOpenArrangeQty(row: PlanStopProductFormRow) { - return Boolean(String(props.contractId || '').trim() && String(row.productId || '').trim()) + return Boolean( + String(props.contractId || '').trim() && + String(row.productId || '').trim() && + !requiresPackageBeforeArrange(row) + ) } function openArrangeQtyDialog(row: PlanStopProductFormRow) { @@ -599,9 +687,14 @@ return } if (!row.productId) return + if (requiresPackageBeforeArrange(row)) { + ElMessage.warning('请先选择包装规格') + return + } arrangeQtyContractId.value = String(props.contractId) arrangeQtyProductId.value = String(row.productId) arrangeQtyProductName.value = String(row.productName || row.standardProductName || '') + arrangeQtyPackageSpec.value = normalizePackageSpec(row.packageSpec) arrangeQtyDialogVisible.value = true } @@ -611,7 +704,9 @@ props.editable, props.contractId, props.excludePlanId, - (props.products || []).map((p) => p.productId).join(',') + (props.products || []) + .map((p) => `${p.productId}:${normalizePackageSpec(p.packageSpec)}`) + .join(',') ] as const, () => { if (!props.editable) return @@ -619,7 +714,7 @@ if (!cid) return for (const row of props.products || []) { const pid = String(row.productId || '').trim() - if (pid) void ensureArrangeQty(pid) + if (pid) void ensureArrangeQty(row) } }, { immediate: true } diff --git a/web-admin/src/utils/pl-contract-qty-reminder.ts b/web-admin/src/utils/pl-contract-qty-reminder.ts index 7bf442b35..b5c0711a1 100644 --- a/web-admin/src/utils/pl-contract-qty-reminder.ts +++ b/web-admin/src/utils/pl-contract-qty-reminder.ts @@ -12,6 +12,7 @@ export type ContractQtyDialogRow = { customerName?: string stopKind?: string productId?: string + packageSpec?: string productName?: string model?: string unit?: string @@ -86,6 +87,7 @@ function normalizeDialogRows( customerName: it.customerName != null ? String(it.customerName) : '', stopKind: it.stopKind != null ? String(it.stopKind) : '', productId: it.productId != null ? String(it.productId) : '', + packageSpec: it.packageSpec != null ? String(it.packageSpec).trim() : '', productName: it.productName != null ? String(it.productName) : '', model: it.model != null ? String(it.model) : '', unit: it.unit != null ? String(it.unit) : '', @@ -141,7 +143,10 @@ export function buildContractQtyDialogHtml( const title = groupTitle(list[0]) const body = list .map((row) => { - const name = escapeHtml(row.productName || row.productId || '—') + const baseName = row.productName || row.productId || '—' + const name = escapeHtml( + row.packageSpec ? `${baseName}(${row.packageSpec})` : baseName + ) const model = escapeHtml(row.model || '—') const unit = escapeHtml(resolveUnitLabel(row.unit)) const contractQty = row.framework diff --git a/web-admin/src/utils/pl-plan-contract-product.ts b/web-admin/src/utils/pl-plan-contract-product.ts index 1a292f659..cdba4b6c7 100644 --- a/web-admin/src/utils/pl-plan-contract-product.ts +++ b/web-admin/src/utils/pl-plan-contract-product.ts @@ -13,6 +13,72 @@ export function collectContractProductIds( return ids } +export type ContractProductRowLite = { + productId?: string | null + packageSpec?: string | null + productName?: string | null + productNo?: string | null + quantity?: number | null + quantityMode?: string | null +} + +export function normalizePackageSpec(value?: string | null): string { + return String(value ?? '').trim() +} + +/** 稳定复合键:productId + '\\0' + 归一化包装 */ +export function productPackageKey( + productId?: string | null, + packageSpec?: string | null +): string { + const pid = String(productId || '').trim() + if (!pid) return '' + return `${pid}\0${normalizePackageSpec(packageSpec)}` +} + +export function collectContractProductRows( + products?: ContractProductRowLite[] | null +): ContractProductRowLite[] { + return (products || []) + .map((row) => ({ + productId: String(row?.productId || '').trim(), + packageSpec: normalizePackageSpec(row?.packageSpec), + productName: row?.productName, + productNo: row?.productNo, + quantity: row?.quantity, + quantityMode: row?.quantityMode + })) + .filter((row) => row.productId) +} + +/** 本合同该产品的包装列表(去重保序,仅非空) */ +export function packageSpecsForProduct( + rows: ContractProductRowLite[] | null | undefined, + productId: string +): string[] { + const pid = String(productId || '').trim() + const out: string[] = [] + const seen = new Set() + for (const row of rows || []) { + if (String(row.productId || '').trim() !== pid) continue + const pkg = normalizePackageSpec(row.packageSpec) + if (!pkg || seen.has(pkg)) continue + seen.add(pkg) + out.push(pkg) + } + return out +} + +export function isPackageAllowedForContractProduct( + rows: ContractProductRowLite[] | null | undefined, + productId: string, + packageSpec?: string | null +): boolean { + const allowed = packageSpecsForProduct(rows, productId) + if (!allowed.length) return true + return allowed.includes(normalizePackageSpec(packageSpec)) +} + /** 保留 productId 落在 allowed 内的装货行;allowed 为空则全部移除(换合同且新合同无产品) */ export function filterProductsByAllowedProductIds( products: T[], @@ -27,23 +93,36 @@ export function filterProductsByAllowedProductIds | null | undefined, - loadProducts: Array<{ productId?: string | null }> | null | undefined + deliveryProducts: + | Array<{ + productId?: string | null + productName?: string | null + packageSpec?: string | null + }> + | null + | undefined, + loadProducts: + | Array<{ productId?: string | null; packageSpec?: string | null }> + | null + | undefined ): string[] { - const loadIds = new Set( - (loadProducts || []).map((row) => String(row?.productId || '').trim()).filter(Boolean) + const loadKeys = new Set( + (loadProducts || []) + .map((row) => productPackageKey(row?.productId, row?.packageSpec)) + .filter(Boolean) ) const names: string[] = [] const seen = new Set() for (const row of deliveryProducts || []) { - const pid = String(row?.productId || '').trim() - if (!pid || loadIds.has(pid) || seen.has(pid)) continue - seen.add(pid) - const name = String(row?.productName || '').trim() || pid - names.push(name) + const key = productPackageKey(row?.productId, row?.packageSpec) + if (!key || loadKeys.has(key) || seen.has(key)) continue + seen.add(key) + const name = String(row?.productName || '').trim() || String(row?.productId || '').trim() + const pkg = normalizePackageSpec(row?.packageSpec) + names.push(pkg ? `${name}(${pkg})` : name) } return names } diff --git a/web-admin/src/utils/pl-plan-stop-form.ts b/web-admin/src/utils/pl-plan-stop-form.ts index 248d95c2a..56631d298 100644 --- a/web-admin/src/utils/pl-plan-stop-form.ts +++ b/web-admin/src/utils/pl-plan-stop-form.ts @@ -6,6 +6,7 @@ import { isPlDibangOptionalVehiclePlate, isPlInternalCustomerName } from '@/utils/pl-internal-customer' +import { normalizePackageSpec } from '@/utils/pl-plan-contract-product' export interface PlanStopProductFormRow { id?: string @@ -214,6 +215,17 @@ function matchPrevStopProduct( return stopProducts[byPlanProductId] } } + const targetPkg = normalizePackageSpec(planRow.packageSpec) + const byProductAndPkg = stopProducts.findIndex( + (row, index) => + !used.has(index) && + String(row.productId) === String(planRow.productId) && + normalizePackageSpec(row.packageSpec) === targetPkg + ) + if (byProductAndPkg >= 0) { + used.add(byProductAndPkg) + return stopProducts[byProductAndPkg] + } const byProductId = stopProducts.findIndex( (row, index) => !used.has(index) && String(row.productId) === String(planRow.productId) )