Просмотр исходного кода

feat(mobile-oa-h5): inline payment detail transfers aligned with web dialog

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 1 неделя назад
Родитель
Сommit
d355cd0640

+ 146 - 0
mobile-oa-h5/src/market/components/PaymentDetailTransfersPanel.vue

@@ -0,0 +1,146 @@
+<template>
+  <section class="payment-detail-transfers">
+    <div class="payment-detail-transfers__title">应缴批次</div>
+    <van-empty
+      v-if="!localSchedules.length"
+      description="暂无应缴批次"
+      image-size="48"
+    />
+    <article
+      v-for="(sch, index) in localSchedules"
+      :key="sch.id || index"
+      class="payment-detail-transfers__period"
+    >
+      <header class="payment-detail-transfers__head">
+        <strong>
+          第 {{ sch.scheduleNo || index + 1 }} 期 · 应缴 {{ formatMoney(sch.dueAmount) }} · {{ displayDate(sch.dueDate) }}
+        </strong>
+        <van-tag class="market-tag" :type="scheduleStatusTone(sch)" round>
+          {{ scheduleStatusLabel(sch) }}
+        </van-tag>
+      </header>
+      <div class="payment-detail-transfers__metrics">
+        <div class="payment-detail-transfers__metric">
+          <span>已缴</span>
+          <b>{{ formatMoney(sch.paidAmount) }}</b>
+        </div>
+        <div class="payment-detail-transfers__metric">
+          <span>欠缴</span>
+          <b>{{ formatMoney(sch.arrearsAmount) }}</b>
+        </div>
+      </div>
+      <ScheduleTransferEditor
+        :schedule="sch"
+        :transfer-types="transferTypes"
+        :market-role="marketRole"
+        :context-id="String(detail.id || '')"
+        :disabled="false"
+        @update:schedule="onScheduleUpdate(index, $event)"
+        @changed="$emit('refresh')"
+      />
+    </article>
+  </section>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import { formatMoney } from '../money.js'
+import { resolveStatusTag, paymentScheduleStatus } from '../status-tag-registry.js'
+import ScheduleTransferEditor from './ScheduleTransferEditor.vue'
+
+const props = defineProps({
+  detail: { type: Object, required: true },
+  marketRole: { type: String, required: true },
+  transferTypes: { type: Array, default: () => [] }
+})
+
+defineEmits(['refresh'])
+
+const localSchedules = ref([])
+
+watch(
+  () => props.detail?.schedules,
+  (schedules) => {
+    localSchedules.value = (Array.isArray(schedules) ? schedules : []).map((row) => ({ ...row }))
+  },
+  { immediate: true, deep: true }
+)
+
+function displayDate(value) {
+  const text = String(value || '').trim()
+  return /^\d{8}$/.test(text)
+    ? `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`
+    : text || '—'
+}
+
+function scheduleStatusLabel(schedule) {
+  return resolveStatusTag(paymentScheduleStatus, schedule?.scheduleStatus).label
+}
+
+function scheduleStatusTone(schedule) {
+  return resolveStatusTag(paymentScheduleStatus, schedule?.scheduleStatus).tagType
+}
+
+function onScheduleUpdate(index, nextSchedule) {
+  const next = [...localSchedules.value]
+  next[index] = nextSchedule
+  localSchedules.value = next
+}
+</script>
+
+<style scoped>
+.payment-detail-transfers {
+  margin: 12px 0 0;
+}
+
+.payment-detail-transfers__title {
+  margin: 0 0 10px;
+  padding: 0 2px;
+  font-size: 15px;
+  font-weight: 700;
+  color: #1f2a26;
+}
+
+.payment-detail-transfers__period {
+  margin-bottom: 12px;
+  border: 1px solid #eef4f1;
+  border-radius: 12px;
+  background: #fafcfb;
+  overflow: hidden;
+}
+
+.payment-detail-transfers__head {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8px;
+  padding: 12px;
+  background: #e9f9f2;
+}
+
+.payment-detail-transfers__head strong {
+  flex: 1;
+  min-width: 140px;
+  font-size: 13px;
+  color: #1f2a26;
+}
+
+.payment-detail-transfers__metrics {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 8px 12px;
+  padding: 12px;
+  border-bottom: 1px dashed #eef4f1;
+}
+
+.payment-detail-transfers__metric span {
+  display: block;
+  color: #6b7c76;
+  font-size: 11px;
+}
+
+.payment-detail-transfers__metric b {
+  font-size: 13px;
+  color: #1f2a26;
+}
+</style>

+ 12 - 15
mobile-oa-h5/src/market/mobile-parity-ui.test.js

@@ -463,7 +463,7 @@ test('market forms share attachment cards and preserve their existing value shap
   assert.match(presignForm, /createPendingMarketAttachment/)
 })
 
-test('payment detail presents every web field and nested transfer field with Chinese labels', () => {
+test('payment detail presents summary overview fields with Chinese labels', () => {
   const sections = getMarketModule('payment').detailSections({
     protocolNo: 'P-1',
     motherName: '张三',
@@ -498,24 +498,21 @@ test('payment detail presents every web field and nested transfer field with Chi
     }]
   })
 
-  assert.deepEqual(sections.map((section) => section.title), ['缴费概览', '应缴批次'])
+  assert.deepEqual(sections.map((section) => section.title), ['缴费概览'])
+  assert.equal(sections.find((section) => section.key === 'schedules'), undefined)
   assert.deepEqual(
     sections[0].fields.map((field) => field.label),
     ['客户', '联系电话', '样本编码', '缴费方式', '合同额', '已缴 / 待缴', '整体进度', '协议状态', '缴费进度']
   )
-  assert.equal(sections[1].cards[0].variant, 'schedule')
-  assert.deepEqual(
-    sections[1].cards[0].fields.map((field) => field.label),
-    ['应缴金额', '应缴日', '已缴金额', '欠缴金额']
-  )
-  const transfer = sections[1].cards[0].cards[0]
-  assert.equal(transfer.variant, 'transfer')
-  assert.equal(transfer.amount, '¥3,000.00')
-  assert.match(transfer.meta, /微信转账/)
-  assert.deepEqual(
-    transfer.fields.map((field) => field.label),
-    ['备注', '财务备注', '转账凭证']
-  )
+})
+
+test('payment detail hosts PaymentDetailTransfersPanel and hides transfer bottom actions', () => {
+  const detail = source('../views/market/MarketModuleDetail.vue')
+  assert.match(detail, /PaymentDetailTransfersPanel/)
+  assert.match(detail, /filterPaymentDetailBottomActions/)
+  const panel = source('./components/PaymentDetailTransfersPanel.vue')
+  assert.match(panel, /ScheduleTransferEditor/)
+  assert.match(panel, /应缴批次/)
 })
 
 test('payment cards reproduce the Web Excel columns and operation labels', () => {

+ 40 - 31
mobile-oa-h5/src/market/module-contracts.test.js

@@ -27,7 +27,7 @@ test('payment chase renders payment and protocol payloads with their native deta
   })
 
   assert.equal(paymentSections[0].key, 'summary')
-  assert.equal(paymentSections[1].cards.length, 1)
+  assert.equal(paymentSections.find((section) => section.key === 'schedules'), undefined)
   assert.equal(protocolSections[0].key, 'protocol')
 })
 
@@ -111,12 +111,16 @@ test('protocol adapters preserve nested detail sections', () => {
     'customer',
     'samples',
     'payment',
-    'storageCert',
-    'insurance',
+    'refundRecords',
     'audits',
-    'statusChange',
-    'attachments'
+    'attachments',
+    'statusChange'
   ])
+  const protocolLabels = protocol.flatMap((section) => (section.fields || []).map((field) => field.label))
+  assert.ok(!protocolLabels.includes('协议编号'))
+  assert.ok(protocolLabels.includes('母亲姓名'))
+  assert.ok(!protocolLabels.includes('新生儿评分(5分钟)'))
+  assert.equal(protocol.at(-1).key, 'statusChange')
 })
 
 test('attachment-bearing detail adapters expose typed values instead of path text', () => {
@@ -135,14 +139,9 @@ test('attachment-bearing detail adapters expose typed values instead of path tex
   const payment = getMarketModule('payment').detailSections({
     schedules: [{ transfers: [{ transferAttachments: 'payment-proof.png' }] }]
   })
-  const paymentFiles = payment[1].cards[0].cards[0].fields.find(
-    (field) => field.label === '转账凭证'
-  )
-  assert.deepEqual(paymentFiles, {
-    label: '转账凭证',
-    type: 'attachments',
-    value: 'payment-proof.png'
-  })
+  assert.equal(payment.find((section) => section.key === 'schedules'), undefined)
+  assert.equal(payment.length, 1)
+  assert.equal(payment[0].key, 'summary')
 
   const presign = getMarketModule('presign').detailSections({
     transfers: [{ transferAttachments: 'deposit-proof.png' }]
@@ -217,7 +216,7 @@ test('payment detail recognizes the converted cell-storage protocol status', ()
   assert.equal(detail[0].fields.find((field) => field.label === '协议状态').value, '转细胞存储')
 })
 
-test('payment detail presents real backend progress, schedule statuses, and combined pay mode', () => {
+test('payment detail presents real backend progress and combined pay mode in summary only', () => {
   const sections = getMarketModule('payment').detailSections({
     payMode: 'djfq',
     paymentProgress: '部分已缴',
@@ -228,13 +227,10 @@ test('payment detail presents real backend progress, schedule statuses, and comb
     ]
   })
   const summary = sections.find((section) => section.key === 'summary')
-  const schedules = sections.find((section) => section.key === 'schedules').cards
 
+  assert.equal(sections.find((section) => section.key === 'schedules'), undefined)
   assert.equal(summary.fields.find((field) => field.label === '缴费方式').value, '趸交+分期')
   assert.equal(summary.fields.find((field) => field.label === '缴费进度').value, '部分已缴')
-  assert.equal(schedules.find((card) => card.key === 'open').statusText, '待缴清')
-  assert.equal(schedules.find((card) => card.key === 'cleared').statusText, '已缴清')
-  assert.equal(schedules.find((card) => card.key === 'void').statusText, '已作废')
 })
 
 test('payment progress dfj aligns with web-admin samplePaymentProgress 待缴费', () => {
@@ -278,13 +274,12 @@ test('protocol read-only history uses Web audit and void type labels', () => {
 
   assert.equal(audits.cards[0].statusText, '审核退回')
   assert.equal(
-    audits.cards[0].fields.find((field) => field.label === '审核状态').value,
-    '审核退回'
-  )
-  assert.equal(
-    status.fields.find((field) => field.label === '作废类型').value,
-    '租户自定义作废'
+    audits.cards[0].fields.find((field) => field.label === '审核状态'),
+    undefined
   )
+  assert.equal(status.fields.length, 1)
+  assert.equal(status.fields[0].value, '暂无状态变更记录')
+  assert.equal(status.cards.length, 0)
 })
 
 test('donation detail shows void type and preserves unknown original values', () => {
@@ -292,15 +287,19 @@ test('donation detail shows void type and preserves unknown original values', ()
     source: 'research_donation',
     basic: {
       donationStatus: 'yzf',
+      voidTime: '2026-10-01 09:30:00',
+      voidDesc: '客户申请',
       voidType: 'legacy-unknown',
       voidTypeLabel: 'legacy-unknown'
     }
   })
   const status = sections.find((section) => section.key === 'statusChange')
 
+  assert.equal(status.cards.length, 1)
+  assert.equal(status.cards[0].statusText, '作废')
   assert.equal(
-    status.fields.find((field) => field.label === '作废类型').value,
-    'legacy-unknown'
+    status.cards[0].fields.find((field) => field.label === '变更说明').value,
+    '作废类型:legacy-unknown;客户申请'
   )
 })
 
@@ -342,7 +341,7 @@ test('detail adapters hide unresolved references and dictionary codes but preser
     ...(section.fields || []).map((field) => field.value),
     ...(section.cards || []).flatMap((card) => card.fields.map((field) => field.value))
   ])
-  assert.ok(values.includes('P-RAW-1'))
+  assert.ok(!values.includes('P-RAW-1'))
   assert.ok(values.includes('S-RAW-1'))
   assert.ok(values.includes('未知人员'))
   assert.ok(values.includes('未知'))
@@ -360,6 +359,7 @@ test('detail adapters hide unresolved references and dictionary codes but preser
   assert.equal(freight[0].fields.find((field) => field.label === '创建人').value, '未知人员')
   assert.equal(freight[1].cards[0].fields.find((field) => field.label === '样本编码').value, 'S-RAW-2')
   assert.equal(freight[1].cards[0].fields.find((field) => field.label === '来源').value, '未知')
+  assert.ok(!freight[1].cards[0].fields.some((field) => field.label === '协议单号'))
 
   const presign = getMarketModule('presign').detailSections({
     protocolNo: 'P-RAW-2', regionId: 'region-raw', regionName: 'region-raw',
@@ -368,7 +368,7 @@ test('detail adapters hide unresolved references and dictionary codes but preser
     payMode: 'raw-pay-mode', orderStatus: 'raw-order-status',
     transfers: [{ transferType: 'raw-transfer', payStatus: 'raw-pay-status' }]
   })
-  assert.equal(presign[0].fields.find((field) => field.label === '协议编号').value, 'P-RAW-2')
+  assert.ok(!presign[0].fields.some((field) => field.label === '协议编号'))
   assert.equal(presign[0].fields.find((field) => field.label === '采集地区').value, '未知')
   assert.equal(presign[0].fields.find((field) => field.label === '市场专员').value, '未知人员')
   assert.equal(presign[0].fields.find((field) => field.label === '缴费方式').value, '未知')
@@ -377,14 +377,23 @@ test('detail adapters hide unresolved references and dictionary codes but preser
   assert.equal(presign[1].cards[0].statusText, '未知')
 
   const payment = getMarketModule('payment').detailSections({
-    protocolNo: 'P-RAW-3', payMode: 'raw-pay-mode', protocolStatus: 'raw-status',
-    progressStatus: 'raw-progress', schedules: [{ scheduleStatus: 'raw-schedule', transfers: [{ transferType: 'raw-transfer', payStatus: 'raw-pay-status' }] }]
+    protocolNo: 'P-RAW-3',
+    motherName: '客户甲',
+    sampleCode: 'S-RAW-3',
+    payMode: 'raw-pay-mode',
+    protocolStatus: 'raw-status',
+    progressStatus: 'raw-progress',
+    schedules: [{ scheduleStatus: 'raw-schedule', transfers: [{ transferType: 'raw-transfer', payStatus: 'raw-pay-status' }] }]
   })
+  const paymentLabels = payment.flatMap((section) => (section.fields || []).map((field) => field.label))
   const paymentValues = payment.flatMap((section) => [
     ...(section.fields || []).map((field) => field.value),
     ...(section.cards || []).flatMap((card) => [card.statusText, ...(card.cards || []).flatMap((nested) => [nested.statusText, ...nested.fields.map((field) => field.value)])])
   ])
-  assert.ok(paymentValues.includes('P-RAW-3'))
+  assert.ok(!paymentLabels.includes('协议号'))
+  assert.ok(!paymentValues.includes('P-RAW-3'))
+  assert.ok(paymentValues.includes('客户甲'))
+  assert.ok(paymentValues.includes('S-RAW-3'))
   for (const raw of ['raw-pay-mode', 'raw-status', 'raw-progress', 'raw-schedule', 'raw-transfer', 'raw-pay-status']) {
     assert.ok(!paymentValues.includes(raw), `must not display ${raw}`)
   }

+ 0 - 77
mobile-oa-h5/src/market/modules/payment.js

@@ -2,48 +2,19 @@ import { formatMoney } from '../money.js'
 import {
   resolveStatusTag,
   samplePaymentProgress,
-  paymentScheduleStatus,
-  payStatus,
   payMode,
   protocolStatus,
   donationStatus
 } from '../status-tag-registry.js'
 
-const TRANSFER_TYPE = Object.freeze({
-  wxzz: '微信转账', wx: '微信转账', wechat: '微信转账',
-  zzbzz: '支付宝转账', zfb: '支付宝转账', alipay: '支付宝转账',
-  yhkzz: '银行卡转账', bank: '银行卡转账',
-  xj: '现金', cash: '现金', lszz: '乐刷'
-})
-
-function label(map, value) {
-  const key = String(value || '').trim()
-  return map[key] || (key ? '未知' : '—')
-}
-
 function money(value) {
   return formatMoney(value)
 }
 
-function displayDate(value) {
-  const text = String(value || '').trim()
-  return /^\d{8}$/.test(text)
-    ? `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`
-    : text || '—'
-}
-
 function progressTag(code) {
   return resolveStatusTag(samplePaymentProgress, code)
 }
 
-function scheduleTag(code) {
-  return resolveStatusTag(paymentScheduleStatus, code)
-}
-
-function transferPayTag(code) {
-  return resolveStatusTag(payStatus, code)
-}
-
 function payModeTag(code) {
   return resolveStatusTag(payMode, code)
 }
@@ -57,47 +28,6 @@ function protocolStatusTag(code) {
   return resolveStatusTag(protocolStatus, key)
 }
 
-function transferCard(transfer, index) {
-  const pay = transferPayTag(transfer?.payStatus)
-  const transferType = label(TRANSFER_TYPE, transfer?.transferType)
-  const transferTime = transfer?.transferTime || '—'
-  return {
-    key: String(transfer?.id || `transfer-${index}`),
-    title: `转账笔 ${index + 1}`,
-    variant: 'transfer',
-    transferIndex: index + 1,
-    amount: money(transfer?.transferAmount),
-    meta: `${transferType} · ${transferTime}`,
-    statusText: pay.label,
-    statusTone: pay.tagType,
-    fields: [
-      { label: '备注', value: transfer?.remark || '—' },
-      { label: '财务备注', value: transfer?.financeRemark || '—' },
-      { label: '转账凭证', type: 'attachments', value: transfer?.transferAttachments || '' }
-    ]
-  }
-}
-
-function scheduleCard(schedule, index) {
-  const status = scheduleTag(schedule?.scheduleStatus)
-  const transfers = schedule?.transfers || []
-  return {
-    key: String(schedule?.id || `schedule-${index}`),
-    title: `第 ${schedule?.scheduleNo || index + 1} 期`,
-    variant: 'schedule',
-    cardsLabel: '转账记录',
-    statusText: status.label,
-    statusTone: status.tagType,
-    fields: [
-      { label: '应缴金额', value: money(schedule?.dueAmount), money: true },
-      { label: '应缴日', value: displayDate(schedule?.dueDate) },
-      { label: '已缴金额', value: money(schedule?.paidAmount), money: true },
-      { label: '欠缴金额', value: money(schedule?.arrearsAmount), money: true }
-    ],
-    cards: transfers.map(transferCard)
-  }
-}
-
 export default {
   key: 'payment',
   resource: 'payments',
@@ -139,7 +69,6 @@ export default {
         key: 'summary',
         title: '缴费概览',
         fields: [
-          { label: '协议号', value: detail?.protocolNo || '—' },
           { label: '客户', value: detail?.motherName || '—' },
           { label: '联系电话', value: detail?.motherPhone || '—' },
           { label: '样本编码', value: detail?.sampleCode || '—' },
@@ -150,12 +79,6 @@ export default {
           { label: '协议状态', value: protocol.label, tag: true, tagType: protocol.tagType },
           { label: '缴费进度', value: progress.label, tag: true, tagType: progress.tagType }
         ]
-      },
-      {
-        key: 'schedules',
-        title: '应缴批次',
-        fields: [],
-        cards: (detail?.schedules || []).map(scheduleCard)
       }
     ]
   },

+ 15 - 0
mobile-oa-h5/src/market/payment-entry.js

@@ -1,6 +1,15 @@
 /** 缴费信息入口进入协议详情时的只读压制 */
 const PROTOCOL_WRITE_CODES = new Set(['edit', 'delete', 'create'])
 
+/** 缴费详情底栏隐藏的转账动作(改由 PaymentDetailTransfersPanel 内联办理) */
+const PAYMENT_TRANSFER_BOTTOM_CODES = new Set([
+  'register-transfer',
+  'edit-transfer',
+  'delete-payment-transfer',
+  'submit',
+  'withdraw'
+])
+
 export function isPaymentEntryView(query) {
   return String(query?.from || '') === 'payment'
 }
@@ -16,3 +25,9 @@ export function filterProtocolActionsForPaymentEntry(actions) {
     (action) => !PROTOCOL_WRITE_CODES.has(actionCode(action))
   )
 }
+
+export function filterPaymentDetailBottomActions(actions) {
+  return (Array.isArray(actions) ? actions : []).filter(
+    (action) => !PAYMENT_TRANSFER_BOTTOM_CODES.has(actionCode(action))
+  )
+}

+ 23 - 1
mobile-oa-h5/src/market/payment-entry.test.js

@@ -2,7 +2,8 @@ import test from 'node:test'
 import assert from 'node:assert/strict'
 import {
   isPaymentEntryView,
-  filterProtocolActionsForPaymentEntry
+  filterProtocolActionsForPaymentEntry,
+  filterPaymentDetailBottomActions
 } from './payment-entry.js'
 
 test('isPaymentEntryView detects from=payment', () => {
@@ -26,3 +27,24 @@ test('filterProtocolActionsForPaymentEntry hides edit and delete', () => {
     [{ code: 'view' }]
   )
 })
+
+test('filterPaymentDetailBottomActions hides transfer bottom actions', () => {
+  assert.deepEqual(
+    filterPaymentDetailBottomActions([
+      'view',
+      'register-transfer',
+      'edit-transfer',
+      'delete-payment-transfer',
+      'submit',
+      'withdraw'
+    ]),
+    ['view']
+  )
+  assert.deepEqual(
+    filterPaymentDetailBottomActions([
+      { code: 'register-transfer', scheduleId: 's1' },
+      { code: 'view' }
+    ]),
+    [{ code: 'view' }]
+  )
+})

+ 36 - 4
mobile-oa-h5/src/views/market/MarketModuleDetail.vue

@@ -29,6 +29,13 @@
         :cards="section.cards"
         :raw="section.raw || section"
       />
+      <PaymentDetailTransfersPanel
+        v-if="context.module === 'payment' && detail"
+        :detail="detail"
+        :market-role="context.marketRole"
+        :transfer-types="paymentTransferTypes"
+        @refresh="loadDetail"
+      />
     </template>
     <van-empty v-else description="暂无详情数据" />
     <template #bottom>
@@ -50,6 +57,7 @@ import {
   deletePresignOrder,
   deletePresignTransfer,
   fetchMarketDetail,
+  fetchMarketFormOptions,
   performMarketAction,
   submitPresignTransfer,
   withdrawPresignTransfer
@@ -57,7 +65,8 @@ import {
 import { collectAllowedActions } from '../../market/action-policy.js'
 import {
   isPaymentEntryView,
-  filterProtocolActionsForPaymentEntry
+  filterProtocolActionsForPaymentEntry,
+  filterPaymentDetailBottomActions
 } from '../../market/payment-entry.js'
 import { marketErrorMessage } from '../../market/market-error.js'
 import { getMarketModule } from '../../market/module-registry.js'
@@ -79,6 +88,7 @@ import {
 import MarketDetailSection from '../../market/components/MarketDetailSection.vue'
 import MarketBottomBar from '../../market/components/MarketBottomBar.vue'
 import MarketPageShell from '../../market/components/MarketPageShell.vue'
+import PaymentDetailTransfersPanel from '../../market/components/PaymentDetailTransfersPanel.vue'
 
 const route = useRoute()
 const router = useRouter()
@@ -94,9 +104,13 @@ const id = computed(() => String(route.params.id || '').trim())
 const moduleTitle = computed(() => MARKET_MODULE_TITLES[context.value.module])
 const roleTitle = computed(() => MARKET_ROLE_TITLES[context.value.marketRole])
 const moduleContract = computed(() => getMarketModule(context.value.module))
-const summarySubtitle = computed(
-  () => detail.value?.subtitle || detail.value?.code || detail.value?.basic?.protocolNo || detail.value?.basic?.donationNo || detail.value?.protocolNo || '业务详情'
-)
+const summarySubtitle = computed(() => {
+  const current = detail.value || {}
+  if (context.value.module === 'protocol') {
+    return current.subtitle || '业务详情'
+  }
+  return current.subtitle || current.code || current.basic?.donationNo || '业务详情'
+})
 const summaryTitle = computed(
   () => detail.value?.motherName || detail.value?.customer?.motherName || detail.value?.card?.motherName || detail.value?.title || detail.value?.name || detail.value?.code || moduleTitle.value
 )
@@ -109,6 +123,7 @@ const statusTone = computed(() => resolveStatusTone(
 ))
 const actionPending = ref(false)
 const pendingAction = ref('')
+const paymentTransferTypes = ref([])
 const allowedActions = computed(() => {
   const raw = collectAllowedActions(
     context.value.module,
@@ -118,6 +133,9 @@ const allowedActions = computed(() => {
   if (context.value.module === 'protocol' && isPaymentEntryView(route.query)) {
     return filterProtocolActionsForPaymentEntry(raw)
   }
+  if (context.value.module === 'payment') {
+    return filterPaymentDetailBottomActions(raw)
+  }
   return raw
 })
 const sections = computed(() => {
@@ -323,6 +341,20 @@ async function loadDetail() {
           : undefined
     })
     detailState.value = finishMarketDetailRequest(detailState.value, requestId, result)
+    if (requestContext.module === 'payment') {
+      try {
+        const options = await fetchMarketFormOptions(requestContext, {
+          businessId: requestIdValue
+        }, { signal: abortController.signal })
+        if (requestId !== requestSequence) return
+        paymentTransferTypes.value = options?.transferTypes || []
+      } catch (optionsError) {
+        if (optionsError?.name === 'AbortError') return
+        paymentTransferTypes.value = []
+      }
+    } else {
+      paymentTransferTypes.value = []
+    }
   } catch (error) {
     if (error?.name === 'AbortError') return
     detailState.value = failMarketDetailRequest(