148 lines
4.5 KiB
TypeScript
148 lines
4.5 KiB
TypeScript
/** 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<unknown> | null): string[] {
|
|
const out: string[] = []
|
|
const seen = new Set<string>()
|
|
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<string>()
|
|
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<string, Set<string>>()
|
|
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 }
|
|
}
|