import { resolvePackageSpecsFromPayload } from '@/utils/crm-package-spec' import { normalizeProductUnitValue } from '@/utils/crm-product-display' /** 从合同明细行收集 CRM productId */ export function collectContractProductIds( products?: Array<{ productId?: string | null }> | null ): string[] { const ids: string[] = [] const seen = new Set() for (const row of products || []) { const id = String(row?.productId || '').trim() if (!id || seen.has(id)) continue seen.add(id) ids.push(id) } return ids } export type ContractProductRowLite = { id?: string | null contractId?: string | null contractNo?: string | null productId?: string | null packageSpec?: string | null packageSpecs?: string[] | null productName?: string | null productNo?: string | null quantity?: number | null quantityMode?: string | null price?: number | null remainingQty?: number | null unit?: string | null model?: string | null productModel?: string | null standardProductName?: string | null aliasName?: string | null brandMode?: string | null brandName?: string | null taxRate?: number | null status?: string | null deliveryMethod?: string | null } export type ContractLineChoice = { id: string contractId: string contractNo: string label: string unit: string } /** 同一产品在当前合同里的明细行,供历史行原位补合同编号。 */ export function contractLineChoicesForProduct( rows: ContractProductRowLite[] | null | undefined, productId?: string | null ): ContractLineChoice[] { const targetId = String(productId || '').trim() const seen = new Set() const choices: ContractLineChoice[] = [] for (const row of rows || []) { if (String(row.productId || '').trim() !== targetId) continue const id = String(row.id || '').trim() const contractId = String(row.contractId || '').trim() const key = `${contractId}:${id}` if (!id || !contractId || seen.has(key)) continue seen.add(key) const contractNo = String(row.contractNo || '').trim() || contractId const specs = resolvePackageSpecsFromPayload(row).filter(Boolean) choices.push({ id, contractId, contractNo, unit: String(row.unit || '').trim() ? normalizeProductUnitValue(row.unit) : '', label: specs.length ? `${contractNo} ${specs.join('、')}` : contractNo }) } return choices } export function collectStopContractIds( contracts?: Array<{ contractId?: string | null }> | null ): string[] { const ids: string[] = [] const seen = new Set() for (const item of contracts || []) { const id = String(item?.contractId || '').trim() if (!id || seen.has(id)) continue seen.add(id) ids.push(id) } return ids } export function unionContractProductIds( byContract: Record | undefined, contractIds: string[] ): string[] { const out: string[] = [] const seen = new Set() for (const cid of contractIds) { for (const raw of byContract?.[cid] || []) { const id = String(raw || '').trim() if (!id || seen.has(id)) continue seen.add(id) out.push(id) } } return out } export function unionContractProductRows( byContract: Record | undefined, contractIds: string[] ): ContractProductRowLite[] { const out: ContractProductRowLite[] = [] for (const cid of contractIds) { out.push(...(byContract?.[cid] || [])) } return out } export function resolveRowPackageSpecs(row?: ContractProductRowLite | null): string[] { return resolvePackageSpecsFromPayload(row) } 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[] { const out: ContractProductRowLite[] = [] for (const row of products || []) { const productId = String(row?.productId || '').trim() if (!productId) continue const specs = resolveRowPackageSpecs(row) const base = { id: row?.id, contractId: row?.contractId, contractNo: row?.contractNo, productId, productName: row?.productName, productNo: row?.productNo, quantity: row?.quantity, quantityMode: row?.quantityMode, price: row?.price, remainingQty: row?.remainingQty, unit: row?.unit, model: row?.model, productModel: row?.productModel, standardProductName: row?.standardProductName, aliasName: row?.aliasName, brandMode: row?.brandMode, brandName: row?.brandName, taxRate: row?.taxRate, status: row?.status, deliveryMethod: row?.deliveryMethod } if (!specs.length) { out.push({ ...base, packageSpec: '' }) continue } for (const packageSpec of specs) { out.push({ ...base, packageSpec: normalizePackageSpec(packageSpec) }) } } return out } /** 本合同该产品的包装列表(去重保序,仅非空) */ 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 for (const pkg of resolveRowPackageSpecs(row)) { if (!pkg || seen.has(pkg)) continue seen.add(pkg) out.push(pkg) } } return out } /** * 装/送货行允许的包装。 * collectContractProductRows 会把一条合同明细按包装拆成多行并共用 id, * 绑了 contractProductId 时要汇总该明细的全部包装,不能只取第一行。 * 未绑定或绑定行不在缓存中时,回退为该产品在所选合同中的全部包装。 */ export function allowedPackageSpecsForStopProduct( rows: ContractProductRowLite[] | null | undefined, product: { productId?: string | null; contractProductId?: string | null } ): string[] { const pid = String(product.productId || '').trim() const boundId = String(product.contractProductId || '').trim() if (!boundId) return packageSpecsForProduct(rows, pid) const boundRows = (rows || []).filter((row) => String(row.id || '').trim() === boundId) if (!boundRows.length) return packageSpecsForProduct(rows, pid) return packageSpecsForProduct(boundRows, pid) } /** 已绑定合同产品行时,单位跟合同走,不能再改。 */ export function stopProductUnitLocked(row: { contractProductId?: string | null }): boolean { return Boolean(String(row.contractProductId || '').trim()) } /** 合同明细上的单位。同一明细被按包装拆成多行时,取第一条有单位的记录。 */ export function boundContractProductUnit( rows: ContractProductRowLite[] | null | undefined, contractProductId?: string | null ): string { const boundId = String(contractProductId || '').trim() if (!boundId) return '' for (const row of rows || []) { if (String(row.id || '').trim() !== boundId) continue const unit = String(row.unit || '').trim() if (unit) return normalizeProductUnitValue(unit) } return '' } /** * 把已绑定行的单位改成合同单位。 * 返回 true 表示本次改写了单位,调用方应提示按合同单位核对数量。 */ export function alignStopProductUnitToContract( row: { contractProductId?: string | null; unit?: string | null }, rows: ContractProductRowLite[] | null | undefined ): boolean { if (!stopProductUnitLocked(row)) return false const contractUnit = boundContractProductUnit(rows, row.contractProductId) if (!contractUnit) return false if (normalizeProductUnitValue(row.unit) === contractUnit) return false row.unit = contractUnit return true } 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[], allowedProductIds: string[] ): T[] { const allowed = new Set( (allowedProductIds || []).map((id) => String(id).trim()).filter(Boolean) ) if (!allowed.size) return [] return (products || []).filter((row) => allowed.has(String(row.productId || '').trim())) } /** * 找出不在装货产品列表中的送货产品(仅提醒,不拦截)。 * 按 productId 比对(同品即视为已在装货中)。 * 不再要求包装规格一致:装货来自合同行常带包装,送货(尤其内部客户主数据选品) * 包装可能为空或尚未填写,用 productId+package 会误报「产品不在装货列表」。 */ export function findDeliveryProductsMissingFromLoad( 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 loadProductIds = new Set( (loadProducts || []) .map((row) => String(row?.productId || '').trim()) .filter(Boolean) ) const names: string[] = [] const seen = new Set() for (const row of deliveryProducts || []) { const pid = String(row?.productId || '').trim() if (!pid || loadProductIds.has(pid) || seen.has(pid)) continue seen.add(pid) const name = String(row?.productName || '').trim() || pid names.push(name) } return names }