108 lines
8.0 KiB
TypeScript
108 lines
8.0 KiB
TypeScript
import type { RowActionItem } from '@/shared/components/RowActions.vue'
|
|
import type { OverseasFee } from './api/overseasFees'
|
|
|
|
export const feePermissions = {
|
|
list: 'platform:overseas-fee:list', edit: 'platform:finance:fee-source:edit', review: 'platform:finance:fee-source:review',
|
|
} as const
|
|
export const businessLabels: Record<string, string> = {
|
|
LASTLEG_ORDER: '尾程订单', WAREHOUSE_TRANSFER: '仓储转运', RETURN_ORDER: '退货', RELABEL_ORDER: '换标',
|
|
US_TRUCK: '本土卡车', LOCAL_LOOSEBOARD: '本土散板', OVERSEAS_PROCUREMENT: '海外采购', DISCARD_ORDER: '废弃',
|
|
OVERSEAS_INBOUND: '入库', WAREHOUSE_SERVICE: '库内服务', DELIVERY_ORDER: '派送', OVERSEAS_CONTAINER: '海外柜次',
|
|
}
|
|
export const reviewLabels: Record<string, string> = { DRAFT: '草稿', PENDING: '待审核', APPROVED: '已通过', REJECTED: '已驳回' }
|
|
/** 已终结来源优先显示终态,避免保留的审核快照误导用户继续审核。 */
|
|
export function feeReviewLabel(fee: Pick<OverseasFee, 'sourceStatus' | 'reviewStatus'>) {
|
|
if (fee.sourceStatus === 'VOIDED') return '已作废'
|
|
if (fee.sourceStatus === 'CANCELLED') return '已取消'
|
|
if (fee.sourceStatus === 'SUPERSEDED') return '已调整'
|
|
return reviewLabels[fee.reviewStatus] || fee.reviewStatus
|
|
}
|
|
/** 失败发生于冻结、实扣或释放的不同阶段,不能一律称为扣费失败。 */
|
|
export function feeFundingErrorLabel(fee: Pick<OverseasFee, 'walletStatus'>) {
|
|
if (fee.walletStatus === 'FREEZE_FAILED') return '冻结失败'
|
|
if (fee.walletStatus === 'RELEASE_FAILED') return '释放失败'
|
|
return '扣费异常'
|
|
}
|
|
/** 业务分类取服务端真实单据类型,派送费项目不改变换标单归属。 */
|
|
export function feeBusinessLabel(fee: Pick<OverseasFee, 'businessType' | 'displayBusinessType'>) {
|
|
const type = fee.displayBusinessType || fee.businessType
|
|
return businessLabels[type] || type
|
|
}
|
|
export const walletLabels: Record<string, string> = {
|
|
FREEZE_PENDING: '锁定中', FROZEN: '已锁定', FREEZE_FAILED: '锁定失败', SETTLE_PENDING: '扣费中', SETTLED: '已扣费',
|
|
SETTLE_FAILED: '扣费失败', RELEASE_PENDING: '释放中', RELEASED: '已释放', RELEASE_FAILED: '释放失败',
|
|
}
|
|
export const processingLabels: Record<string, string> = { COMPLETED: '已完成', FAILED: '处理失败' }
|
|
/** 余额实扣即完成;账单推送按接收回执判断,自动重试期间保留真实失败。 */
|
|
export function feeProcessingStatus(fee: Pick<OverseasFee, 'sourceStatus' | 'publicationStatus' | 'reviewStatus' | 'walletStatus' | 'deliveryStatus' | 'deliveryErrorMessage'>) {
|
|
// 业务关闭不代表冻结资金已释放;失败必须保留可见,方便恢复处理。
|
|
if (fee.walletStatus === 'RELEASE_FAILED' || (['FREEZE_PENDING', 'RELEASE_PENDING'].includes(fee.walletStatus || '')
|
|
&& (fee.deliveryStatus === 'DEAD' || !!fee.deliveryErrorMessage?.trim()))) return 'FAILED'
|
|
if (['FREEZE_PENDING', 'RELEASE_PENDING'].includes(fee.walletStatus || '')) return 'PROCESSING'
|
|
if (['VOIDED', 'SUPERSEDED', 'CANCELLED'].includes(fee.sourceStatus) || fee.reviewStatus === 'REJECTED') return 'CLOSED'
|
|
if (fee.walletStatus === 'SETTLED') return 'COMPLETED'
|
|
if (fee.deliveryStatus === 'DEAD' || (['PENDING', 'PROCESSING'].includes(fee.deliveryStatus || '') && fee.deliveryErrorMessage?.trim()) || fee.walletStatus?.endsWith('_FAILED')) return 'FAILED'
|
|
if (fee.deliveryStatus === 'PENDING' || fee.deliveryStatus === 'PROCESSING' || fee.walletStatus?.endsWith('_PENDING')) return 'PROCESSING'
|
|
if (fee.publicationStatus === 'PUBLISHED') return 'COMPLETED'
|
|
return 'WAITING'
|
|
}
|
|
/** 应付完成仅代表财务已接收,不能用完成文案暗示供应商已收款。 */
|
|
export const feeProcessingLabel = (fee: OverseasFee) => feeProcessingStatus(fee) === 'COMPLETED' && fee.direction === 'PAYABLE'
|
|
? '推送成功' : processingLabels[feeProcessingStatus(fee)] || '—'
|
|
|
|
/** 只提醒当前有效版本的同项目,已驳回记录仍用于解释重新录费原因。 */
|
|
export function sameProjectFees(rows: OverseasFee[], code: string, direction: string, excludeId?: string) {
|
|
return rows.filter(row => row.feeItemCode === code && row.direction === direction && row.sourceStatus === 'ACTIVE' && row.sourceFeeId !== excludeId)
|
|
}
|
|
/** 重复收费提示同时交代原币金额和审核结果,不把不同币种直接相加。 */
|
|
export const sameProjectFeeSummary = (fee: OverseasFee) => `${feeMoney(fee.originalAmount, fee.originalCurrency)} · ${reviewLabels[fee.reviewStatus] || fee.reviewStatus}`
|
|
export const retryResultLabels: Record<string, string> = { SUCCESS: '重试成功', FAILED: '重试后仍失败', SUBMITTED: '已提交重试' }
|
|
export const actionLabels: Record<string, string> = {
|
|
REVIEW: '审核', APPROVE: '审核通过', REJECT: '驳回', RETRY_FREEZE: '重试锁定', RETRY_SETTLE: '重试扣费',
|
|
RETRY_RELEASE: '重试释放', SUBMIT: '提交审核', VOID: '作废', ADJUST: '调整',
|
|
RETRY_PUBLISH: '重试推送', RETRY_FUNDING_DELIVERY: '重试资金指令', ADJUST_RENT: '调整仓租',
|
|
}
|
|
export function feeStatus(fee: Pick<OverseasFee, 'sourceStatus' | 'publicationStatus' | 'deliveryStatus' | 'deliveryKind'> & { collectionMode?: string | null; walletStatus?: string | null }) {
|
|
if (fee.sourceStatus === 'VOIDED') return '已作废'
|
|
if (fee.sourceStatus === 'SUPERSEDED') return '已调整'
|
|
if (fee.sourceStatus === 'CANCELLED') return '已取消'
|
|
if (fee.collectionMode === 'WALLET' && fee.walletStatus === 'SETTLED') return '已扣费'
|
|
if (fee.deliveryKind === 'FUNDING' && fee.deliveryStatus === 'DEAD') return '资金指令发送失败'
|
|
if (fee.deliveryKind === 'FEE' && fee.deliveryStatus) {
|
|
if (fee.deliveryStatus === 'DEAD') return '推送失败'
|
|
if (fee.deliveryStatus !== 'PUBLISHED') return fee.collectionMode === 'WALLET' ? '已扣费,待推送' : '推送中'
|
|
}
|
|
if (fee.collectionMode === 'BILL') return fee.publicationStatus === 'PUBLISHED' ? '已推送结算' : '待推送结算'
|
|
return fee.walletStatus ? walletLabels[fee.walletStatus] || fee.walletStatus : fee.collectionMode === 'WALLET' ? '待锁定' : '—'
|
|
}
|
|
export function feeActions(fee: OverseasFee, can: (permission: string) => boolean, busy = false): RowActionItem[] {
|
|
const closed = fee.reviewStatus === 'REJECTED' || ['VOIDED', 'SUPERSEDED', 'CANCELLED'].includes(fee.sourceStatus)
|
|
const canReview = can(feePermissions.review)
|
|
const visibleActions = fee.allowedActions.filter(action => action in actionLabels && !['APPROVE', 'REJECT', 'CANCEL'].includes(action))
|
|
.filter(action => !closed || ['RETRY_RELEASE', 'RETRY_FUNDING_DELIVERY'].includes(action))
|
|
.filter(action => can(['VOID'].includes(action) ? feePermissions.review : feePermissions.edit))
|
|
.map(action => ({ key: action, label: actionLabels[action]!, priority: 20,
|
|
tone: ['VOID'].includes(action) ? 'danger' as const : 'primary' as const, disabled: busy }))
|
|
// 审核通过与驳回共用一个入口;取消不在业务单据费用行提供。
|
|
if (!closed && canReview && fee.allowedActions.some(action => ['APPROVE', 'REJECT'].includes(action))) {
|
|
visibleActions.push({ key: 'REVIEW', label: actionLabels.REVIEW!, priority: 90, tone: 'primary', disabled: busy })
|
|
}
|
|
return [{ key: 'DETAIL', label: '详情', priority: 100, disabled: busy }, ...visibleActions]
|
|
}
|
|
export function feeMoney(amount: number | string | null | undefined, currency = '') {
|
|
if (amount == null) return '—'
|
|
return `${currency} ${Number(amount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`.trim()
|
|
}
|
|
export function feeTime(value?: string | null) {
|
|
if (!value) return '—'
|
|
// 只有服务端明确标记时区的时间才转换,兼容旧的无时区数据。
|
|
if (/(?:Z|[+-]\d{2}:\d{2})$/i.test(value)) {
|
|
const date = new Date(value)
|
|
if (!Number.isNaN(date.getTime())) {
|
|
const pad = (part: number) => String(part).padStart(2, '0')
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
|
}
|
|
}
|
|
return value.replace('T', ' ').slice(0, 19)
|
|
}
|