feat(web-admin): per-sample payment UI and refund registration on protocol form

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-03 11:13:32 +08:00
1 parent a328b9ff2d
commit 4e71a13165
5 files changed
+1280 -19

No files matched your search

@@ -0,0 +1,73 @@
/** 按样本缴费 · 工具函数(payment_model_version=2) */
export type SamplePaymentLike = {
contractAmount?: number | null
sampleCode?: string
payMode?: string
schedules?: ScheduleLike[]
}
export type TransferLike = {
payStatus?: string
transferAmount?: number | null
}
export type ScheduleLike = {
dueAmount?: number | null
transfers?: TransferLike[]
}
export function sumSampleContractAmounts(payments: SamplePaymentLike[]): number {
return payments.reduce((s, p) => s + (Number(p.contractAmount) || 0), 0)
}
export function protocolTotalMismatchHint(
total: number | null | undefined,
sampleSum: number
): string | null {
if (total == null || Number.isNaN(Number(total))) return null
if (Math.abs(Number(total) - sampleSum) > 0.015) {
return `协议总金额 ${Number(total).toFixed(2)} 与样本合同合计 ${sampleSum.toFixed(2)} 不一致,请核对`
}
return null
}
export function schedulePaidArrears(
dueAmount: number,
transfers: TransferLike[]
): { paid: number; arrears: number } {
const paid = (transfers || [])
.filter((t) => t.payStatus === 'yjf')
.reduce((s, t) => s + (Number(t.transferAmount) || 0), 0)
return { paid, arrears: Math.round((dueAmount - paid) * 100) / 100 }
}
/** 单样本默认定价:沿用原「1 个样本」价目 */
export function resolvePerSamplePreset(payMode: 'dj' | 'fq'): {
contractAmount: number
installmentCount?: number
installmentAmounts?: number[]
} | null {
if (payMode === 'dj') return { contractAmount: 22880 }
if (payMode === 'fq') {
return {
contractAmount: 25880,
installmentCount: 6,
installmentAmounts: [6480, 3880, 3880, 3880, 3880, 3880]
}
}
return null
}
export function parseInstallmentRatios(raw: unknown): number[] {
if (Array.isArray(raw)) return raw.map((v) => Number(v) || 0)
if (typeof raw === 'string' && raw.trim()) {
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) return parsed.map((v) => Number(v) || 0)
} catch {
/* ignore */
}
}
return []
}