203 lines
7.1 KiB
TypeScript
203 lines
7.1 KiB
TypeScript
import { fetchProductDetail } from '@/api/crm/product'
|
||
import type { PlanProduct } from '@/api/pl'
|
||
|
||
export type HazardFlagProduct = {
|
||
productId?: string
|
||
productName?: string
|
||
isToxic?: number
|
||
isExplosive?: number
|
||
is_toxic?: number
|
||
is_explosive?: number
|
||
}
|
||
|
||
export type HazardCertSlotKey = 'purchaseCertUrl' | 'transportCertUrl' | 'legalUseCertUrl'
|
||
|
||
export interface HazardCertSlot {
|
||
key: HazardCertSlotKey
|
||
label: string
|
||
}
|
||
|
||
const HAZARD_CERT_SLOT_LABELS: Record<HazardCertSlotKey, string> = {
|
||
purchaseCertUrl: '购买证',
|
||
transportCertUrl: '运输证',
|
||
legalUseCertUrl: '合法证明'
|
||
}
|
||
|
||
function isTruthyHazardFlag(value: unknown) {
|
||
return value === 1 || value === true || value === '1'
|
||
}
|
||
|
||
export function resolveProductHazardFlags(product: HazardFlagProduct) {
|
||
const isToxic =
|
||
isTruthyHazardFlag(product.isToxic) || isTruthyHazardFlag(product.is_toxic)
|
||
const isExplosive =
|
||
isTruthyHazardFlag(product.isExplosive) || isTruthyHazardFlag(product.is_explosive)
|
||
return { isToxic, isExplosive }
|
||
}
|
||
|
||
export function isHazardProduct(product: Pick<PlanProduct, 'isToxic' | 'isExplosive'>) {
|
||
const flags = resolveProductHazardFlags(product)
|
||
return flags.isToxic || flags.isExplosive
|
||
}
|
||
|
||
export function getHazardProductLabel(product: Pick<PlanProduct, 'isToxic' | 'isExplosive'>) {
|
||
const flags = resolveProductHazardFlags(product)
|
||
if (flags.isExplosive) return '易制爆'
|
||
if (flags.isToxic) return '易制毒'
|
||
return ''
|
||
}
|
||
|
||
/** 按产品危化属性返回所需证书槽位(单一规则源) */
|
||
export function getRequiredCertSlots(product: HazardFlagProduct): HazardCertSlot[] {
|
||
const { isToxic, isExplosive } = resolveProductHazardFlags(product)
|
||
const slots: HazardCertSlot[] = []
|
||
if (isToxic) {
|
||
slots.push({ key: 'purchaseCertUrl', label: HAZARD_CERT_SLOT_LABELS.purchaseCertUrl })
|
||
slots.push({ key: 'transportCertUrl', label: HAZARD_CERT_SLOT_LABELS.transportCertUrl })
|
||
}
|
||
if (isExplosive) {
|
||
slots.push({ key: 'legalUseCertUrl', label: HAZARD_CERT_SLOT_LABELS.legalUseCertUrl })
|
||
}
|
||
return slots
|
||
}
|
||
|
||
export function getProductCertUrl(product: PlanProduct, key: HazardCertSlotKey): string {
|
||
const value = product[key]
|
||
return typeof value === 'string' ? value.trim() : ''
|
||
}
|
||
|
||
export function isProductCertSlotFilled(product: PlanProduct, key: HazardCertSlotKey): boolean {
|
||
return Boolean(getProductCertUrl(product, key))
|
||
}
|
||
|
||
export function isProductCertComplete(product: PlanProduct): boolean {
|
||
const slots = getRequiredCertSlots(product)
|
||
if (!slots.length) return true
|
||
return slots.every(slot => isProductCertSlotFilled(product, slot.key))
|
||
}
|
||
|
||
/** 产品名称后追加易制毒/易制爆标识:毒、爆,或「毒 爆」 */
|
||
export function formatPlProductNameWithHazardMarks(product: HazardFlagProduct) {
|
||
const name = product.productName || ''
|
||
const { isToxic, isExplosive } = resolveProductHazardFlags(product)
|
||
const marks: string[] = []
|
||
if (isToxic) marks.push('毒')
|
||
if (isExplosive) marks.push('爆')
|
||
if (!marks.length) return name
|
||
return `${name}${marks.join(' ')}`
|
||
}
|
||
|
||
/** 从 CRM 产品主数据读取易制毒/易制爆标识(覆盖计划产品快照字段) */
|
||
export async function enrichPlanProductsHazardFromCrm<T extends HazardFlagProduct & { productId: string }>(
|
||
products: T[]
|
||
): Promise<T[]> {
|
||
if (!products.length) return products
|
||
|
||
const lookupIds = [
|
||
...new Set(products.map(product => product.productId).filter(Boolean))
|
||
]
|
||
if (!lookupIds.length) return products
|
||
|
||
const hazardMap = new Map<string, { isToxic: number; isExplosive: number; taxRate?: number }>()
|
||
await Promise.all(
|
||
lookupIds.map(async productId => {
|
||
try {
|
||
const res = await fetchProductDetail({ id: productId })
|
||
const data = (res as { data?: Record<string, unknown> }).data ?? res
|
||
const row = data as Record<string, unknown>
|
||
const rawTaxRate = row.taxRate ?? row.tax_rate
|
||
hazardMap.set(productId, {
|
||
isToxic: Number(row.isToxic ?? row.is_toxic ?? 0),
|
||
isExplosive: Number(row.isExplosive ?? row.is_explosive ?? 0),
|
||
taxRate: rawTaxRate != null && rawTaxRate !== '' ? Number(rawTaxRate) : undefined
|
||
})
|
||
} catch {
|
||
// 单条产品查询失败时跳过
|
||
}
|
||
})
|
||
)
|
||
|
||
return products.map(product => {
|
||
const fromCrm = hazardMap.get(product.productId)
|
||
if (!fromCrm) return product
|
||
return {
|
||
...product,
|
||
isToxic: fromCrm.isToxic,
|
||
isExplosive: fromCrm.isExplosive,
|
||
taxRate: product.taxRate ?? fromCrm.taxRate
|
||
}
|
||
})
|
||
}
|
||
|
||
export function findMissingHazardCerts(products: PlanProduct[]) {
|
||
return products.filter(p => isHazardProduct(p) && !isProductCertComplete(p))
|
||
}
|
||
|
||
export function planHasHazardProducts(products: PlanProduct[] | undefined) {
|
||
return (products ?? []).some(p => isHazardProduct(p))
|
||
}
|
||
|
||
export function isHazardCertReady(plan: { hazardCertReady?: number } | null | undefined) {
|
||
return Number(plan?.hazardCertReady ?? 0) === 1
|
||
}
|
||
|
||
/** 含危化品且尚未确认证书齐备(人工确认或全部所需证书已上传)时,阻断安排运输 */
|
||
export function shouldBlockTransportForHazardCert(
|
||
plan: { hazardCertReady?: number; products?: PlanProduct[] } | null | undefined,
|
||
products?: PlanProduct[]
|
||
) {
|
||
const prods = products ?? plan?.products
|
||
if (!planHasHazardProducts(prods)) return false
|
||
if (isHazardCertReady(plan)) return false
|
||
if (!findMissingHazardCerts(prods ?? []).length) return false
|
||
return true
|
||
}
|
||
|
||
export function buildMissingHazardCertReadyMessage(
|
||
plan: { hazardCertReady?: number; products?: PlanProduct[] } | null | undefined,
|
||
products?: PlanProduct[]
|
||
) {
|
||
if (!shouldBlockTransportForHazardCert(plan, products)) return ''
|
||
const prods = products ?? plan?.products ?? []
|
||
const missingCertsMsg = buildMissingHazardCertMessage(prods)
|
||
if (missingCertsMsg) return missingCertsMsg
|
||
return '该计划危化品证书尚未确认齐备,请点击「证书齐备」或补全所需证书'
|
||
}
|
||
|
||
function formatMissingCertLabels(product: PlanProduct): string {
|
||
const missing = getRequiredCertSlots(product)
|
||
.filter(slot => !isProductCertSlotFilled(product, slot.key))
|
||
.map(slot => slot.label)
|
||
return missing.join('、')
|
||
}
|
||
|
||
export function buildMissingHazardCertMessage(products: PlanProduct[]) {
|
||
const missing = findMissingHazardCerts(products)
|
||
if (!missing.length) return ''
|
||
const details = missing
|
||
.map(p => `「${p.productName}」须补充:${formatMissingCertLabels(p)}`)
|
||
.join(';')
|
||
return `以下产品证书尚未齐全:${details}`
|
||
}
|
||
|
||
export const HAZARD_CERT_REQUIREMENT_LINES = [
|
||
'易制毒:购买证、运输证',
|
||
'易制爆:合法证明'
|
||
] as const
|
||
|
||
export const HAZARD_CERT_READY_CONFIRM_HINT =
|
||
'计划内产品仅分为易制毒、易制爆或普通产品,同一产品不会兼具两种危化属性。' +
|
||
HAZARD_CERT_REQUIREMENT_LINES.join(';') +
|
||
'。'
|
||
|
||
/** 证书齐备确认弹窗正文(换行列表展示) */
|
||
export function buildHazardCertReadyConfirmMessage(): string {
|
||
return [
|
||
'确认该计划危化品证书已齐备?',
|
||
'',
|
||
...HAZARD_CERT_REQUIREMENT_LINES,
|
||
'',
|
||
'确认后可安排运输;之后仍可在本页补充上传附件。'
|
||
].join('<br>')
|
||
}
|