fix(mobile-oa-h5): align protocol and chase tags with registry mirror

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-15 10:01:43 +08:00
1 parent 79f9c8785b
commit 7ba336169a
4 files changed
+170 -62

No files matched your search

+59 -17
View File
@@ -1,5 +1,12 @@
import { formatMoney } from './money.js'
import { resolveStatusTag, samplePaymentProgress, payMode } from './status-tag-registry.js'
import {
resolveStatusTag,
samplePaymentProgress,
payMode,
protocolStatus,
donationStatus,
paymentChaseStatus
} from './status-tag-registry.js'
const SEARCH_PLACEHOLDERS = Object.freeze({
protocol: '搜索姓名 / 身份证号 / 电话',
@@ -28,12 +35,7 @@ const ACTION_LABELS = Object.freeze({
unlink: '取消样本关联'
})
const PAY_MODE_LABELS = Object.freeze({ dj: '趸交', fq: '分期', fgd: '分改趸', djfq: '趸交+分期' })
const SOURCE_LABELS = Object.freeze({ cell_storage: '细胞存储', research_donation: '科研捐赠' })
const PROTOCOL_STATUS_LABELS = Object.freeze({
dsx: '待生效', dqr: '待确认', sxz: '生效中', ygq: '已过期', yzf: '已作废',
sdfq: '属地废弃', zckc: '转细胞存储', zky: '转科研'
})
const NEWBORN_SEX_LABELS = Object.freeze({ '1': '男', '0': '女' })
const DELIVERY_MODE_LABELS = Object.freeze({ sc: '顺产', pgc: '剖宫产' })
@@ -43,12 +45,37 @@ const label = (map, value) => {
if (!key) return '—'
return map[key] || (/[㐀-鿿]/u.test(key) ? key : '未知')
}
const protocolStatusText = (item) => {
const code = String(item.protocolStatus || item.donationStatus || item.status || '').trim()
const base = label(PROTOCOL_STATUS_LABELS, code)
if (code !== 'yzf') return base
function protocolStatusCode(item) {
if (item?.source === 'research_donation') {
return String(item.donationStatus || item.protocolStatus || item.status || '').trim()
}
return String(item?.protocolStatus || item?.donationStatus || item?.status || '').trim()
}
/** 细胞存储走 protocolStatus;捐赠侧码(如 zckc)走 donationStatus;作废色仍按码取 */
function protocolBizStatusTag(item) {
const code = protocolStatusCode(item)
if (!code) return { label: '—', tagType: 'info' }
if (item?.source === 'research_donation') {
if (donationStatus[code]) return resolveStatusTag(donationStatus, code)
if (protocolStatus[code]) return resolveStatusTag(protocolStatus, code)
return resolveStatusTag(donationStatus, code)
}
if (protocolStatus[code]) return resolveStatusTag(protocolStatus, code)
if (donationStatus[code]) return resolveStatusTag(donationStatus, code)
return resolveStatusTag(protocolStatus, code)
}
const protocolStatusDisplay = (item) => {
const tag = protocolBizStatusTag(item)
const code = protocolStatusCode(item)
if (code !== 'yzf') return { ...tag, display: tag.label }
const voidTypeLabel = String(item.voidTypeLabel || item.voidType || '').trim()
return voidTypeLabel ? `${base}(${voidTypeLabel})` : base
return {
...tag,
display: voidTypeLabel ? `${tag.label}(${voidTypeLabel})` : tag.label
}
}
const referenceName = (name, id) => {
const displayName = String(name ?? '').trim()
@@ -92,18 +119,20 @@ function attachmentPresence(urls) {
function protocolCard(item) {
const actions = allowed(item, ['edit', 'delete'])
const isDonation = item.source === 'research_donation'
const status = protocolStatusText(item)
const statusTag = protocolStatusDisplay(item)
const status = statusTag.display
if (isDonation) {
const collectDate = String(item.sampleCollectTime || '').slice(0, 10)
return {
title: text(item.motherName || item.customerName),
subtitle: `${label(SOURCE_LABELS, item.source)} · ${text(item.labStorageCode || item.sampleCode)}`,
status,
statusTone: statusTag.tagType,
fields: [
field('采集日期', collectDate || item.donationDate),
field('当月核量号', item.monthlyVerifyNo),
field('实验室流转编号', item.labStorageCode || item.sampleCode, { wide: true }),
field('协议状态', status, { tag: true }),
field('协议状态', status, { tag: true, tagType: statusTag.tagType }),
field('废弃原因', item.voidDesc),
field('母亲姓名', item.motherName || item.customerName),
field('样本类型', item.sampleTypeName),
@@ -136,8 +165,9 @@ function protocolCard(item) {
title: text(item.motherName || item.customerName),
subtitle: subtitleParts.join(' · '),
status,
statusTone: statusTag.tagType,
fields: [
field('状态', status, { tag: true }),
field('状态', status, { tag: true, tagType: statusTag.tagType }),
field('协议编号', item.protocolNo, { wide: true }),
field('登记日期', item.registerDate || item.donationDate),
field('母亲姓名', item.motherName || item.customerName),
@@ -160,22 +190,34 @@ function protocolCard(item) {
}
function chaseCard(item) {
const chaseCode = String(item.chaseStatus || '').trim()
const chaseTag = chaseCode ? resolveStatusTag(paymentChaseStatus, chaseCode) : null
const timing = item.chaseStatus === 'overdue'
? `已逾期 ${Number(item.daysOverdue || 0)} 天`
: `${Number(item.daysUntilDue || 0)} 天内到期`
const mode = resolveStatusTag(payMode, item.payMode)
const protocol = protocolBizStatusTag({
protocolStatus: item.protocolStatus,
source: 'cell_storage'
})
const chaseField = chaseTag
? field('催缴状态', timing, { tag: true, tagType: chaseTag.tagType })
: field('催缴状态', timing, { tag: true })
return {
title: `${text(item.motherName)} · 第 ${text(item.scheduleNo, '1')} 期`,
subtitle: text(item.sampleCode),
status: timing,
...(chaseTag ? { statusTone: chaseTag.tagType } : {}),
fields: [
field('协议号', item.protocolNo, { wide: true }), field('客户', item.motherName),
field('联系电话', item.motherPhone), field('样本编码', item.sampleCode, { wide: true }),
field('市场专员', personName(item.salesUserName, item.salesUserId)), field('缴费方式', label(PAY_MODE_LABELS, item.payMode), { tag: true }),
field('市场专员', personName(item.salesUserName, item.salesUserId)),
field('缴费方式', mode.label, { tag: true, tagType: mode.tagType }),
field('期次', `第 ${text(item.scheduleNo, '1')} 期`), field('应缴日', item.dueDate),
field('应缴金额', money(item.dueAmount), { money: true }), field('已缴金额', money(item.paidAmount), { money: true }),
field('欠缴金额', money(item.arrearsAmount), { money: true }),
field('催缴状态', timing, { tag: true }),
field('协议状态', label(PROTOCOL_STATUS_LABELS, item.protocolStatus), { tag: true })
chaseField,
field('协议状态', protocol.label, { tag: true, tagType: protocol.tagType })
],
actions: ['view-payment', 'view-protocol']
}
+92 -40
View File
@@ -4,34 +4,30 @@ import {
productionReceptionFieldValue,
resolveProductionReceptionFields
} from '../protocol-production-reception.js'
import {
resolveStatusTag,
protocolStatus,
donationStatus,
payMode,
payStatus,
paymentScheduleStatus,
auditStatus
} from '../status-tag-registry.js'
const STATUS_LABELS = Object.freeze({
dsx: '待生效', dqr: '待确认', sxz: '生效中', ygq: '已过期',
yzf: '已作废', sdfq: '属地废弃', zky: '转科研', zckc: '转细胞存储'
})
const PAY_MODE_LABELS = Object.freeze({ dj: '趸交', fq: '分期', fgd: '分改趸', djfq: '趸交+分期' })
const SEX_LABELS = Object.freeze({ '1': '男', '0': '女' })
const DELIVERY_LABELS = Object.freeze({ sc: '顺产', pgc: '剖宫产' })
const SAMPLE_STATUS_LABELS = Object.freeze({
ycj: '已采集', jcz: '检测中', yrk: '已入库', yzf: '已作废'
})
const AUDIT_STATUS_LABELS = Object.freeze({
shtg: '审核通过', shth: '审核退回', dsh: '待审核'
})
const VOID_TYPE_LABELS = Object.freeze({
khhc: '客户毁存', jcbhg: '检测不合格'
})
const PAYMENT_STATUS_LABELS = Object.freeze({
dfj: '已付款', jfqrz: '付款确认中', yjf: '付款完成',
tfqrz: '退费确认中', ytf: '已退费'
})
const TRANSFER_TYPE_LABELS = Object.freeze({
wxzz: '微信转账', wx: '微信转账', wechat: '微信转账',
zzbzz: '支付宝转账', zfb: '支付宝转账', alipay: '支付宝转账',
yhkzz: '银行卡转账', bank: '银行卡转账',
xj: '现金', cash: '现金', lszz: '乐刷'
})
const SCHEDULE_STATUS_LABELS = Object.freeze({ open: '待缴清', cleared: '已缴清', void: '已作废' })
function value(source, key) {
const result = source?.[key]
@@ -43,6 +39,36 @@ function label(map, source) {
return map[key] || (key ? (/[㐀-鿿]/u.test(key) ? key : '未知') : '—')
}
/** 细胞存储走 protocolStatus;捐赠侧码(如 zckc)走 donationStatus */
function bizStatusTag(code, source) {
const key = String(code || '').trim()
if (!key) return { label: '—', tagType: 'info' }
if (source === 'research_donation') {
if (donationStatus[key]) return resolveStatusTag(donationStatus, key)
if (protocolStatus[key]) return resolveStatusTag(protocolStatus, key)
return resolveStatusTag(donationStatus, key)
}
if (protocolStatus[key]) return resolveStatusTag(protocolStatus, key)
if (donationStatus[key]) return resolveStatusTag(donationStatus, key)
return resolveStatusTag(protocolStatus, key)
}
function payModeTag(code) {
return resolveStatusTag(payMode, code)
}
function transferPayTag(code) {
return resolveStatusTag(payStatus, code)
}
function scheduleTag(code) {
return resolveStatusTag(paymentScheduleStatus, code)
}
function auditTag(code) {
return resolveStatusTag(auditStatus, code)
}
function voidTypeText(source) {
const backendLabel = String(source?.voidTypeLabel || '').trim()
if (backendLabel) return backendLabel
@@ -304,7 +330,7 @@ function sampleLifecycleCards(item, source) {
}
function paymentTransferCard(transfer, transferIndex) {
const statusText = label(PAYMENT_STATUS_LABELS, transfer?.payStatus)
const pay = transferPayTag(transfer?.payStatus)
const transferType = transferTypeText(transfer)
const transferTime = value(transfer, 'transferTime')
return {
@@ -314,7 +340,8 @@ function paymentTransferCard(transfer, transferIndex) {
transferIndex: transferIndex + 1,
amount: value(transfer, 'transferAmount'),
meta: `${transferType} · ${transferTime}`,
statusText,
statusText: pay.label,
statusTone: pay.tagType,
fields: [
{ label: '备注', value: value(transfer, 'remark') },
{ label: '财务备注', value: value(transfer, 'financeRemark') },
@@ -324,12 +351,15 @@ function paymentTransferCard(transfer, transferIndex) {
}
function paymentScheduleCards(pay) {
return (pay?.schedules || []).map((schedule, scheduleIndex) => ({
return (pay?.schedules || []).map((schedule, scheduleIndex) => {
const status = scheduleTag(schedule?.scheduleStatus)
return {
key: String(schedule?.id || `schedule-${scheduleIndex}`),
title: `第 ${schedule?.scheduleNo || scheduleIndex + 1} 期`,
variant: 'schedule',
cardsLabel: '转账记录',
statusText: label(SCHEDULE_STATUS_LABELS, schedule?.scheduleStatus),
statusText: status.label,
statusTone: status.tagType,
fields: [
{ label: '应缴金额', value: value(schedule, 'dueAmount'), money: true },
{ label: '应缴日期', value: value(schedule, 'dueDate') },
@@ -337,16 +367,20 @@ function paymentScheduleCards(pay) {
{ label: '欠缴金额', value: value(schedule, 'arrearsAmount'), money: true }
],
cards: (schedule?.transfers || []).map(paymentTransferCard)
}))
}
})
}
function legacyPaymentDetailCards(payment, details) {
return groupLegacyPaymentPeriods({ payment, details }).map((period, index) => ({
return groupLegacyPaymentPeriods({ payment, details }).map((period, index) => {
const status = scheduleTag(period.scheduleStatus)
return {
key: `legacy-payment-${period.scheduleNo || index + 1}`,
title: `第 ${period.scheduleNo || index + 1} 期`,
variant: 'schedule',
cardsLabel: '转账记录',
statusText: label(SCHEDULE_STATUS_LABELS, period.scheduleStatus),
statusText: status.label,
statusTone: status.tagType,
fields: [
{ label: '应缴金额', value: period.dueAmount == null ? '—' : String(period.dueAmount), money: true },
{ label: '应缴日期', value: period.dueDate || '—' },
@@ -359,7 +393,8 @@ function legacyPaymentDetailCards(payment, details) {
transferIndex
)
)
}))
}
})
}
export default {
@@ -394,7 +429,7 @@ export default {
const status = detail?.source === 'research_donation'
? basic?.donationStatus
: basic?.protocolStatus
return label(STATUS_LABELS, status)
return bizStatusTag(status, detail?.source).label
},
detailSections(detail) {
if (Array.isArray(detail?.sections)) {
@@ -405,13 +440,19 @@ export default {
const statusKey = source === 'research_donation' ? 'donationStatus' : 'protocolStatus'
const dateKey = source === 'research_donation' ? 'donationDate' : 'registerDate'
const numberKey = source === 'research_donation' ? 'donationNo' : 'protocolNo'
const bizStatus = bizStatusTag(basic?.[statusKey], source)
const values = [
{
key: 'protocol',
title: source === 'research_donation' ? '捐赠信息' : '协议信息',
fields: [
{ label: source === 'research_donation' ? '捐赠编号' : '协议编号', value: value(basic, numberKey) },
{ label: source === 'research_donation' ? '捐赠状态' : '协议状态', value: label(STATUS_LABELS, basic?.[statusKey]), tag: true },
{
label: source === 'research_donation' ? '捐赠状态' : '协议状态',
value: bizStatus.label,
tag: true,
tagType: bizStatus.tagType
},
{ label: source === 'research_donation' ? '捐赠日期' : '登记日期', value: value(basic, dateKey) },
{ label: '采集专员', value: person(basic?.collectorUserName, basic?.collectorUserId) },
...(source === 'cell_storage' ? [{ label: '协议总金额', value: value(basic, 'protocolTotalAmount') }] : []),
@@ -510,13 +551,13 @@ export default {
value: `¥${amt.toFixed(2)}(${cnt} 笔);与预签同一数据,不自动写入样本缴费`
}]
: [],
cards: depositTransfers.map((tr, index) => ({
cards: depositTransfers.map((tr, index) => {
const pay = transferPayTag(tr?.payStatus)
return {
key: String(tr?.id || `deposit-${index}`),
title: `意向金转账 ${index + 1}`,
statusText: label(
{ dfj: '已付款', jfqrz: '付款确认中', yjf: '付款完成', tfqrz: '退费确认中', ytf: '已退费' },
tr?.payStatus
),
statusText: pay.label,
statusTone: pay.tagType,
fields: [
{ label: '转账金额', value: value(tr, 'transferAmount') },
{ label: '转账时间', value: value(tr, 'transferTime') },
@@ -524,9 +565,10 @@ export default {
{ label: '转账凭证', value: transferAttachmentValue(tr), type: 'attachments' },
{ label: '备注', value: value(tr, 'remark') },
{ label: '财务备注', value: value(tr, 'financeRemark') },
{ label: '状态', value: label(PAYMENT_STATUS_LABELS, tr?.payStatus), tag: true }
{ label: '状态', value: pay.label, tag: true, tagType: pay.tagType }
]
}))
}
})
})
}
if (detail?.source === 'cell_storage' || detail?.payment || detail?.samplePayments) {
@@ -536,11 +578,13 @@ export default {
key: 'payment',
title: '缴费信息',
fields: [],
cards: samplePays.map((pay, index) => ({
cards: samplePays.map((pay, index) => {
const mode = payModeTag(pay?.payMode)
return {
key: String(pay?.id || pay?.sampleCode || `pay-${index}`),
title: pay?.sampleCode || `样本缴费 ${index + 1}`,
fields: [
{ label: '缴费模式', value: label(PAY_MODE_LABELS, pay?.payMode), tag: true },
{ label: '缴费模式', value: mode.label, tag: true, tagType: mode.tagType },
{ label: '合同金额', value: value(pay, 'contractAmount') },
...(String(pay?.payMode || '') === 'dj'
? [{ label: '趸交截止时间', value: value(pay, 'lumpSumDeadline') }]
@@ -556,20 +600,24 @@ export default {
})))
],
cards: paymentScheduleCards(pay)
}))
}
})
})
} else {
const legacyDetails = Array.isArray(detail?.paymentDetails) ? detail.paymentDetails : []
const legacyMode = payModeTag(detail?.payment?.payMode)
values.push({
key: 'payment',
title: '缴费信息',
fields: fields(detail?.payment, [
['payMode', '缴费模式', { map: (item) => label(PAY_MODE_LABELS, item), tag: true }],
fields: [
{ label: '缴费模式', value: legacyMode.label, tag: true, tagType: legacyMode.tagType },
...fields(detail?.payment, [
['contractAmount', '合同金额'], ['installmentCount', '分期期数'],
['installmentIntervalMonths', '每期间隔(月)'], ['lumpSumDeadline', '趸交截止时间'],
['installmentStartDate', '分期开始时间'], ['installmentAdvanceDays', '提前催缴预警(天)'],
['remark', '缴费备注']
]),
])
],
cards: legacyPaymentDetailCards(detail?.payment || {}, legacyDetails)
})
}
@@ -624,17 +672,21 @@ export default {
key: 'audits',
title: '审核记录',
fields: audits.length ? [] : [{ label: '记录', value: '暂无审核记录' }],
cards: audits.map((row, index) => ({
cards: audits.map((row, index) => {
const audit = auditTag(row?.auditStatus)
return {
key: String(row?.id || `audit-${index}`),
title: `审核 ${index + 1}`,
statusText: label(AUDIT_STATUS_LABELS, row?.auditStatus),
statusText: audit.label,
statusTone: audit.tagType,
fields: [
{ label: '审核状态', value: label(AUDIT_STATUS_LABELS, row?.auditStatus), tag: true },
{ label: '审核状态', value: audit.label, tag: true, tagType: audit.tagType },
{ label: '审核人', value: person(row?.auditorUserName, row?.auditorUserId) },
{ label: '审核意见', value: value(row, 'auditRemark') },
{ label: '时间', value: value(row, 'createTime') }
]
}))
}
})
})
const commonStatusFields = [
['effectiveTime', '协议生效时间'],
@@ -1,11 +1,15 @@
/** 协议缴费期内转账:与 Web SamplePaymentPanel 规则对齐的纯函数 */
import { normalizeMoney } from './money.js'
import { resolveStatusTag, payStatus } from './status-tag-registry.js'
export const PAY_STATUS_LABELS = Object.freeze({
dfj: '已付款',
jfqrz: '付款确认中',
yjf: '付款完成'
})
/** 转账笔 payStatus 文案(含 tfqrz/ytf);与 status-tag-registry.payStatus 同步 */
export function payStatusLabel(code) {
return resolveStatusTag(payStatus, code || 'dfj').label
}
export const PAY_STATUS_LABELS = Object.freeze(
Object.fromEntries(Object.keys(payStatus).map((code) => [code, payStatus[code].label]))
)
const SCHEDULE_ACTIONS = new Set(['register-transfer'])
const TRANSFER_ACTION_ALIASES = Object.freeze({
@@ -10,6 +10,8 @@ import {
emptyTransferRow,
isTransferLocked,
patchTransfersAt,
payStatusLabel,
PAY_STATUS_LABELS,
validateTransferRow
} from './protocol-sample-transfer.js'
@@ -20,6 +22,14 @@ test('empty transfer starts as pending payment', () => {
assert.equal(isTransferLocked({ payStatus: 'yjf' }), true)
})
test('payStatus labels cover refund codes via registry', () => {
assert.equal(payStatusLabel('dfj'), '已付款')
assert.equal(payStatusLabel('tfqrz'), '退费确认中')
assert.equal(payStatusLabel('ytf'), '已退费')
assert.equal(PAY_STATUS_LABELS.tfqrz, '退费确认中')
assert.equal(PAY_STATUS_LABELS.ytf, '已退费')
})
test('save requires persisted protocol and schedule ids', () => {
assert.throws(() => assertCanSaveTransfer({}), /请先保存协议/)
assert.throws(() => assertCanSaveTransfer({ protocolId: 'p1' }), /请先保存协议/)