/** Contract / quote product package_spec codec — mirror backend `crm_package_spec.py`. */ export const MAX_PACKAGE_SPECS_PER_LINE = 20 export interface PackageQtyBreakdownItem { packageSpec?: string archivedQty?: number | null arrangedUnarchivedQty?: number | null arrangedTotalQty?: number | null } export function normalizePackageSpecs(values?: Iterable | null): string[] { const out: string[] = [] const seen = new Set() for (const raw of values || []) { const s = String(raw ?? '').trim() if (!s || seen.has(s)) continue seen.add(s) out.push(s) if (out.length >= MAX_PACKAGE_SPECS_PER_LINE) break } return out } /** JSON array or legacy scalar → string[] */ export function decodePackageSpecs(value: unknown): string[] { if (value == null) return [] if (Array.isArray(value)) return normalizePackageSpecs(value) const text = String(value).trim() if (!text) return [] if (text.startsWith('[')) { try { const parsed = JSON.parse(text) if (Array.isArray(parsed)) return normalizePackageSpecs(parsed) } catch { // fall through to scalar } } return [text] } /** Prefer API `packageSpecs` when present; fallback to scalar `packageSpec`. */ export function resolvePackageSpecsFromPayload(data?: { packageSpecs?: string[] | null packageSpec?: string | null } | null): string[] { if (!data) return [] if (data.packageSpecs != null) return normalizePackageSpecs(data.packageSpecs) return decodePackageSpecs(data.packageSpec) } export function formatPackageSpecsJoin( data?: { packageSpecs?: string[] | null; packageSpec?: string | null } | null, separator = '、' ): string { const specs = resolvePackageSpecsFromPayload(data) return specs.length ? specs.join(separator) : '' } export function hasMultiPackageQtyBreakdown(product?: { packageQtyBreakdown?: PackageQtyBreakdownItem[] | null } | null): boolean { return (product?.packageQtyBreakdown?.length ?? 0) > 1 } export function resolveLineRemainingQty(product?: { remainingQty?: number | null quantity?: number | null arrangedTotalQty?: number | null } | null): number | null { if (product?.remainingQty != null && !Number.isNaN(Number(product.remainingQty))) { return Number(product.remainingQty) } const qty = Number(product?.quantity) const arranged = Number(product?.arrangedTotalQty ?? 0) if (!Number.isNaN(qty) && qty > 0) { return qty - arranged } return null } /** Collect union of all line package specs (dedupe, preserve order). */ export function collectAllContractPackageSpecs( products?: Array<{ packageSpecs?: string[] | null; packageSpec?: string | null }> | null ): string[] { const out: string[] = [] const seen = new Set() for (const p of products || []) { for (const spec of resolvePackageSpecsFromPayload(p)) { if (seen.has(spec)) continue seen.add(spec) out.push(spec) } } return out } export function normalizeContractProductPackageFields< T extends { packageSpecs?: string[] | null; packageSpec?: string | null } >(product: T): T { const specs = resolvePackageSpecsFromPayload(product) product.packageSpecs = specs product.packageSpec = specs[0] || '' return product } export function buildPackageSpecsPayload(product: { packageSpecs?: string[] | null packageSpec?: string | null }) { const specs = product.packageSpecs?.length ? normalizePackageSpecs(product.packageSpecs) : resolvePackageSpecsFromPayload(product) return { packageSpecs: specs, packageSpec: specs[0] || '' } } export function validateContractProductPackageSpecs( products: Array<{ productId?: string | number | null packageSpecs?: string[] | null packageSpec?: string | null }> ): { ok: true } | { ok: false; message: string } { const seenByProduct = new Map>() for (let i = 0; i < products.length; i++) { const product = products[i] const rowNum = i + 1 const specs = resolvePackageSpecsFromPayload(product) if (!specs.length) { return { ok: false, message: `第${rowNum}行产品的包装规格不能为空` } } const pid = String(product.productId || '').trim() if (!pid) continue if (!seenByProduct.has(pid)) seenByProduct.set(pid, new Set()) const seen = seenByProduct.get(pid)! for (const spec of specs) { if (seen.has(spec)) { return { ok: false, message: `同一产品的包装规格不能重复:${spec}` } } seen.add(spec) } } return { ok: true } }