feat: finish P0 per-sample payment and presign order integration
Wire finance transfer list, protocol view v2 readonly payment, and mark design/plan as P0 implemented. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
a9112414c9
commit
e7677d1661
7 files changed
+1950
-283
No files matched your search
@@ -331,6 +331,38 @@ async def submit_payment_transfer_finance(
|
||||
return error_response(400, str(e))
|
||||
|
||||
|
||||
@router.get("/payment-transfers")
|
||||
async def list_payment_transfers(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1, le=100, alias="pageSize"),
|
||||
payStatus: str = Query("dfj", alias="payStatus"),
|
||||
keyword: Optional[str] = None,
|
||||
installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"),
|
||||
installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"),
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""按转账笔状态分页(缴费信息看板 / 财务共用)。"""
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
items, total = psp_svc.list_transfers_for_finance(
|
||||
db,
|
||||
pay_status=payStatus,
|
||||
tenant_no=tenant_no,
|
||||
keyword=keyword,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
user_info=user,
|
||||
due_date_start=installmentDueDateStart,
|
||||
due_date_end=installmentDueDateEnd,
|
||||
)
|
||||
return success_response(
|
||||
{"list": items, "total": total, "page": page, "pageSize": page_size}
|
||||
)
|
||||
except PermissionError as e:
|
||||
return error_response(403, str(e))
|
||||
|
||||
|
||||
@router.get("/payment-transfers/receipt-confirm")
|
||||
async def list_receipt_confirm(
|
||||
page: int = Query(1, ge=1),
|
||||
|
||||
@@ -0,0 +1,653 @@
|
||||
# 按样本缴费 + 预签约订单 Implementation Plan(P0)
|
||||
|
||||
> **状态**:P0 Tasks 1–10 已落地(2026-10-03)。开票改挂 transfer / 旧单迁移属 P1。
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 正式协议按样本建立缴费方案(应缴批次 + 多笔转账、财务按笔确认);协议手填总金额与退费登记;新增预签约订单(意向金按笔确认、转正式仅提示);财务页下线退费确认 Tab。
|
||||
|
||||
**Architecture:** 新表三层:`biz_protocol_sample_payment` → `biz_protocol_payment_schedule` → `biz_protocol_payment_transfer`;预签约独立主表 + `biz_presign_transfer`;协议 `payment_model_version` 区分新旧;旧 `biz_protocol_payment`/`detail` 仅服务 version=1。财务待办合并查询正式转账笔与预签约意向金笔。
|
||||
|
||||
**Tech Stack:** MySQL 5.7 · FastAPI · SQLAlchemy · Vue 3 · Element Plus · pnpm · pytest
|
||||
|
||||
**Design spec:** `docs/superpowers/specs/2026-10-03-per-sample-payment-presign-design.md`
|
||||
**现状分析:** `docs/superpowers/specs/2026-10-03-cell-storage-protocol-sample-payment-analysis.md`
|
||||
|
||||
**本计划范围 = 设计文档 P0-a + P0-b + P0-c。** 开票改挂 transfer、旧单迁移工具属 P1,不在本计划强制交付。
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `docs/sql/biz_per_sample_payment_presign.sql` | **新建** 全量表结构 DDL |
|
||||
| `backend/app/models/models.py` | ORM:新表 + protocol 增量字段 |
|
||||
| `backend/app/crud/protocol_sample_payment.py` | **新建** 样本方案/批次/转账 CRUD + 财务确认 |
|
||||
| `backend/app/crud/protocol_refund_record.py` | **新建** 退费登记 |
|
||||
| `backend/app/crud/presign_order.py` | **新建** 预签约 + 意向金转账 + convert |
|
||||
| `backend/app/api/protocol_sample_payment.py` | **新建** 正式缴费/转账财务 API |
|
||||
| `backend/app/api/presign_order.py` | **新建** 预签约 API |
|
||||
| `backend/app/api/__init__.py` / `app/main.py` | 注册路由 |
|
||||
| `backend/app/api/menu.py` | 菜单:预签约 + 财务文案/隐藏退费 |
|
||||
| `backend/app/api/collection_protocol.py` | 详情 bundle 带 version=2 缴费;总金额 |
|
||||
| `backend/tests/test_protocol_sample_payment.py` | **新建** 核心单测 |
|
||||
| `backend/tests/test_presign_order.py` | **新建** |
|
||||
| `web-admin/src/api/protocol-sample-payment.ts` | **新建** |
|
||||
| `web-admin/src/api/presign-order.ts` | **新建** |
|
||||
| `web-admin/src/router/modules/sqxp.ts` | 预签约路由;财务菜单 title |
|
||||
| `web-admin/src/views/cell-storage/presign-order/**` | **新建** 预签约列表/表单 |
|
||||
| `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` | 按样本缴费 UI、总金额、意向金提示 |
|
||||
| `web-admin/src/views/cell-storage/protocol/ProtocolRefundRecordPanel.vue` | **新建** 退费登记块 |
|
||||
| `web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue` | 按转账笔看板(version=2) |
|
||||
| `web-admin/src/views/cell-storage/payment-confirm/index.vue` | 合并正式+预签约待办;隐藏退费 Tab |
|
||||
| `web-admin/src/utils/protocol-sample-payment.ts` | **新建** 批次已缴/欠缴计算、总金额提示 |
|
||||
| `web-admin/src/utils/status-tag-registry.ts` | 预签约状态等 Tag |
|
||||
|
||||
---
|
||||
|
||||
## 依赖顺序
|
||||
|
||||
```text
|
||||
Task 1 DDL + ORM
|
||||
→ Task 2 样本缴费 CRUD + 转账财务状态机(单测)
|
||||
→ Task 3 正式缴费 API + 注册路由
|
||||
→ Task 4 退费登记 CRUD/API
|
||||
→ Task 5 协议详情/保存接入 version=2 + 总金额
|
||||
→ Task 6 前端协议表单按样本缴费 + 退费登记 UI
|
||||
→ Task 7 财务确认页改造(按笔、去退费 Tab)
|
||||
→ Task 8 预签约后端(含 convert + 意向金财务)
|
||||
→ Task 9 预签约前端 + 菜单路由
|
||||
→ Task 10 缴费信息 Tab / 查看弹窗适配 + 冒烟清单
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: DDL 与 ORM
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/sql/biz_per_sample_payment_presign.sql`
|
||||
- Modify: `backend/app/models/models.py`
|
||||
|
||||
- [ ] **Step 1: 编写 DDL**
|
||||
|
||||
在 `docs/sql/biz_per_sample_payment_presign.sql` 中创建(utf8mb4,含公共字段:`tenant_no, order_num, remark, create_by, create_dept_id, create_time, update_by, update_time, del_flag`):
|
||||
|
||||
1. `biz_collection_protocol` 增加:
|
||||
- `protocol_total_amount` DECIMAL(18,2) NULL
|
||||
- `source_presign_order_id` VARCHAR(64) NULL
|
||||
- `payment_model_version` SMALLINT NOT NULL DEFAULT 2
|
||||
(已有行若需兼容旧逻辑,上线脚本可将存量更新为 `1`)
|
||||
|
||||
2. `biz_protocol_sample_payment`
|
||||
3. `biz_protocol_payment_schedule`
|
||||
4. `biz_protocol_payment_transfer`(`pay_status` 默认 `dfj`;无退费字段)
|
||||
5. `biz_protocol_refund_record`
|
||||
6. `biz_presign_order`
|
||||
7. `biz_presign_order_sample_type`
|
||||
8. `biz_presign_transfer`
|
||||
|
||||
索引建议:
|
||||
|
||||
- `(tenant_no, collection_protocol_id, del_flag)` on sample_payment / schedule / transfer / refund
|
||||
- `(tenant_no, sample_code, del_flag)` on sample_payment / transfer
|
||||
- `(tenant_no, pay_status, del_flag)` on transfer / presign_transfer
|
||||
- `(tenant_no, protocol_no, del_flag)` on presign_order
|
||||
|
||||
字段定义以设计文档 §5 为准。
|
||||
|
||||
- [ ] **Step 2: 增加 ORM 类**
|
||||
|
||||
在 `models.py` 中新增对应 `class`,命名:
|
||||
|
||||
- `BizProtocolSamplePayment`
|
||||
- `BizProtocolPaymentSchedule`
|
||||
- `BizProtocolPaymentTransfer`
|
||||
- `BizProtocolRefundRecord`
|
||||
- `BizPresignOrder`
|
||||
- `BizPresignOrderSampleType`
|
||||
- `BizPresignTransfer`
|
||||
|
||||
并在 `BizCollectionProtocol` 上增加三个新 Column。
|
||||
|
||||
- [ ] **Step 3: 本地执行 DDL**(对开发库)
|
||||
|
||||
```bash
|
||||
# 在已配置的 MySQL 上执行 docs/sql/biz_per_sample_payment_presign.sql
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/sql/biz_per_sample_payment_presign.sql backend/app/models/models.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: add per-sample payment and presign order schema
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 样本缴费 CRUD 与转账状态机(TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/crud/protocol_sample_payment.py`
|
||||
- Create: `backend/tests/test_protocol_sample_payment.py`
|
||||
|
||||
- [ ] **Step 1: 写失败单测(核心规则)**
|
||||
|
||||
```python
|
||||
# backend/tests/test_protocol_sample_payment.py
|
||||
"""纯函数/服务规则单测;DB 集成可按项目惯例用 fixture。"""
|
||||
from decimal import Decimal
|
||||
|
||||
# 若逻辑放在 services,先测服务层;示例:
|
||||
from app.services.protocol_transfer_status import (
|
||||
can_submit_receipt,
|
||||
can_confirm_receipt,
|
||||
schedule_paid_and_arrears,
|
||||
)
|
||||
|
||||
def test_submit_only_dfj():
|
||||
assert can_submit_receipt("dfj") is True
|
||||
assert can_submit_receipt("jfqrz") is False
|
||||
assert can_submit_receipt("yjf") is False
|
||||
|
||||
def test_confirm_only_jfqrz():
|
||||
assert can_confirm_receipt("jfqrz") is True
|
||||
assert can_confirm_receipt("dfj") is False
|
||||
|
||||
def test_schedule_arrears():
|
||||
paid, arrears = schedule_paid_and_arrears(
|
||||
due=Decimal("1000"),
|
||||
confirmed_transfers=[Decimal("300"), Decimal("200")],
|
||||
)
|
||||
assert paid == Decimal("500")
|
||||
assert arrears == Decimal("500")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行确认失败**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate
|
||||
pytest tests/test_protocol_sample_payment.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL(模块不存在)
|
||||
|
||||
- [ ] **Step 3: 实现 `app/services/protocol_transfer_status.py` + CRUD 骨架**
|
||||
|
||||
`protocol_sample_payment.py` 至少包含:
|
||||
|
||||
```python
|
||||
def upsert_sample_payment(...) -> BizProtocolSamplePayment: ...
|
||||
def replace_schedules(db, sample_payment_id, schedules: list) -> None: ...
|
||||
def add_transfer(...) -> BizProtocolPaymentTransfer: ...
|
||||
def update_transfer_draft(...) -> ... # 仅 dfj
|
||||
def submit_receipt_finance(db, transfer_id, tenant_no, user_id) -> ...
|
||||
def confirm_receipt(...) -> ... # jfqrz → yjf
|
||||
def reject_receipt(...) -> ... # jfqrz → dfj
|
||||
def list_transfers_for_finance(db, *, pay_status, tenant_no, keyword, page, page_size, user_info): ...
|
||||
def get_sample_payments_bundle(db, collection_protocol_id, tenant_no) -> list[dict]: ...
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 提交:`pay_status==dfj` 且 `transfer_amount>0` 且 `transfer_type` 非空 → `jfqrz`
|
||||
- 存在任一批次下转账为 `jfqrz` 时,`replace_schedules` / 改 `pay_mode` 抛 `ValueError`
|
||||
- 关联键写入 `sample_code`;`protocol_sample_id` 可选
|
||||
|
||||
- [ ] **Step 4: 再跑单测**
|
||||
|
||||
```bash
|
||||
pytest tests/test_protocol_sample_payment.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/services/protocol_transfer_status.py \
|
||||
backend/app/crud/protocol_sample_payment.py \
|
||||
backend/tests/test_protocol_sample_payment.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: add sample payment schedule and transfer status rules
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 正式缴费 / 财务 API
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/api/protocol_sample_payment.py`
|
||||
- Modify: `backend/app/api/__init__.py`
|
||||
- Modify: `backend/app/main.py`
|
||||
|
||||
- [ ] **Step 1: 实现路由(前缀 `/web/admin`)**
|
||||
|
||||
| 方法 | 路径 |
|
||||
|------|------|
|
||||
| GET | `/collection-protocols/{id}/sample-payments` |
|
||||
| PUT | `/collection-protocols/{id}/sample-payments`(body: 方案+批次+可选草稿转账) |
|
||||
| POST | `/payment-transfers`(在某 schedule 下新增转账) |
|
||||
| PUT | `/payment-transfers/{id}`(草稿) |
|
||||
| POST | `/payment-transfers/{id}/submit-receipt-finance` |
|
||||
| GET | `/payment-transfers/receipt-confirm` |
|
||||
| GET | `/payment-transfers/receipt-confirmed` |
|
||||
| POST | `/payment-transfers/{id}/receipt-confirm` |
|
||||
| POST | `/payment-transfers/{id}/receipt-reject` |
|
||||
|
||||
鉴权:`require_admin_or_super_admin`;列表 `apply_data_scope`。
|
||||
|
||||
- [ ] **Step 2: 注册 router**(与现有 `collection_protocol` 相同方式 include)
|
||||
|
||||
- [ ] **Step 3: 手工用 Swagger `/docs` 冒烟一条 dfj→jfqrz→yjf**
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/api/protocol_sample_payment.py backend/app/api/__init__.py backend/app/main.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: expose per-sample payment and transfer finance APIs
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 协议退费登记
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/crud/protocol_refund_record.py`
|
||||
- Create: `backend/app/api/protocol_refund_record.py`(或并入 collection_protocol)
|
||||
- Test: `backend/tests/test_protocol_refund_record.py`
|
||||
|
||||
- [ ] **Step 1: 单测 — 创建登记必填金额>0**
|
||||
|
||||
```python
|
||||
def test_refund_amount_must_be_positive():
|
||||
with pytest.raises(ValueError):
|
||||
validate_refund_amount(0)
|
||||
with pytest.raises(ValueError):
|
||||
validate_refund_amount(-1)
|
||||
validate_refund_amount(0.01)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 实现 CRUD + API**
|
||||
|
||||
```
|
||||
GET/POST /collection-protocols/{id}/refund-records
|
||||
PUT/DELETE /protocol-refund-records/{id}
|
||||
```
|
||||
|
||||
不修改任何 transfer 的 `pay_status`。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: add protocol-level refund registration API
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 协议保存 / 详情接入 version=2
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/collection_protocol.py`
|
||||
- Modify: `backend/app/crud/collection_protocol.py`(`get_protocol_detail_bundle`、`_protocol_to_dict`)
|
||||
|
||||
- [ ] **Step 1: `_protocol_to_dict` / bundle 增加**
|
||||
|
||||
```python
|
||||
"protocolTotalAmount": float(...) if ... else None,
|
||||
"sourcePresignOrderId": ...,
|
||||
"paymentModelVersion": int(getattr(obj, "payment_model_version", 1) or 1),
|
||||
"samplePayments": [...], # version==2
|
||||
"refundRecords": [...],
|
||||
"presignDepositHint": {...}, # 若有来源预签约:已确认意向金合计
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建/更新**
|
||||
|
||||
- 新协议默认 `payment_model_version=2`
|
||||
- body 可收 `protocol_total_amount`、`sample_payments`
|
||||
- **version=2 时不再要求旧 `payment`/`payment_details` 必填**;旧字段写入仅当 version=1
|
||||
- 保存顺序建议:当事人 → 采集 → **样本** → **sample_payments(校验后)**(因方案挂 sample_code,样本须先落库占码)
|
||||
注意:与旧「缴费先于样本」不同;新模型转账未占号池,样本编码仍在 `save_samples`,缴费方案在样本之后保存即可。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: wire protocol bundle to per-sample payment model v2
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 前端协议表单 — 按样本缴费 + 退费登记
|
||||
|
||||
**Files:**
|
||||
- Create: `web-admin/src/api/protocol-sample-payment.ts`
|
||||
- Create: `web-admin/src/utils/protocol-sample-payment.ts`
|
||||
- Create: `web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue`
|
||||
- Create: `web-admin/src/views/cell-storage/protocol/ProtocolRefundRecordBlock.vue`
|
||||
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue`
|
||||
- Modify: `web-admin/src/utils/status-tag-registry.ts`(如需)
|
||||
|
||||
- [ ] **Step 1: API 与工具函数**
|
||||
|
||||
```typescript
|
||||
// protocol-sample-payment.ts 工具
|
||||
export function sumSampleContractAmounts(payments: { contractAmount?: 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: { payStatus?: string; transferAmount?: 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 }
|
||||
}
|
||||
```
|
||||
|
||||
冒烟:
|
||||
|
||||
```bash
|
||||
cd web-admin && pnpm exec tsx -e "
|
||||
import { protocolTotalMismatchHint, schedulePaidArrears } from './src/utils/protocol-sample-payment.ts'
|
||||
console.log(protocolTotalMismatchHint(100, 90))
|
||||
console.log(schedulePaidArrears(1000, [{payStatus:'yjf',transferAmount:400}]))
|
||||
"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `SamplePaymentPanel.vue`**
|
||||
|
||||
- 按 `formData.samples` 渲染每个样本一张卡片(标题用样本类型名 + sampleCode)
|
||||
- 字段:payMode、contractAmount、分期参数
|
||||
- 批次表 + 展开转账笔;转账可「提交财务确认」
|
||||
- 分改趸按钮:调用与旧 `applyPayModeSwitch` 同类逻辑,但作用在单样本(可新建 `protocol-sample-pay-mode-switch.ts`)
|
||||
|
||||
- [ ] **Step 3: 改造 `ProtocolFormDialog` 缴费 Tab**
|
||||
|
||||
- 顶部:来源预签约意向金提示(`presignDepositHint`)
|
||||
- 协议总金额手填;旁路显示样本合计;保存前 `protocolTotalMismatchHint` → `ElMessage.warning` 不阻断
|
||||
- `payment_model_version===2`(新建默认)走 `SamplePaymentPanel`;不再使用整单一套旧 paymentDetails 为主路径
|
||||
- 隐藏或移除依赖「协议级一套 djfq」的强制校验;样本数与单样本价可用新 preset(单样本价目)
|
||||
|
||||
- [ ] **Step 4: `ProtocolRefundRecordBlock`**
|
||||
|
||||
- 列表 + 新增弹窗:金额、时间、备注、附件(MinIO 卡片上传,folder 如 `protocol-refund-record`)
|
||||
- 调用 refund-records API
|
||||
|
||||
- [ ] **Step 5: 浏览器手工验收**(两样本分别分期/趸交、3 笔转账提交)
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): per-sample payment UI and refund registration on protocol form
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 财务确认页改造
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/cell-storage/payment-confirm/index.vue`
|
||||
- Modify: `web-admin/src/views/cell-storage/payment-confirm/RefundConfirmTab.vue`(停用)
|
||||
- Modify: `web-admin/src/api/protocol-sample-payment.ts`(财务 list)
|
||||
- Modify: `web-admin/src/api/presign-order.ts`(可先留空,Task 8/9 接意向金)
|
||||
- Modify: `web-admin/src/router/modules/sqxp.ts`(title 可选)
|
||||
- Modify: `backend/app/api/menu.py`(title + 保持一致)
|
||||
|
||||
- [ ] **Step 1: 缴费确认 Tab 改调 `/payment-transfers/receipt-confirm`**
|
||||
|
||||
卡片字段:协议号、母亲、样本编码、期次、转账金额/时间/方式、来源=`formal`。
|
||||
|
||||
- [ ] **Step 2: 隐藏退费确认 Tab**
|
||||
|
||||
```typescript
|
||||
// 仅保留 缴费确认 | 开票申请;RefundConfirmTab 不再挂载
|
||||
const tabs = [
|
||||
{ key: 'receipt', label: '缴费确认' },
|
||||
{ key: 'invoice', label: '开票申请' },
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 菜单文案改为「缴费财务确认及开票」**(`sqxp.ts` + `menu.py`)
|
||||
|
||||
- [ ] **Step 4: 开票暂保持旧 detail API(P1 再改);在 Tab 上加简短注释或 TODO 指向设计 P1——**仅注释,不阻塞 P0**
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): finance confirm by transfer and remove refund confirm tab
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 预签约后端
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/crud/presign_order.py`
|
||||
- Create: `backend/app/api/presign_order.py`
|
||||
- Create: `backend/tests/test_presign_order.py`
|
||||
- Modify: `backend/app/main.py` / `api/__init__.py`
|
||||
|
||||
- [ ] **Step 1: 单测 convert 与意向金状态**
|
||||
|
||||
```python
|
||||
def test_presign_transfer_same_status_machine():
|
||||
assert can_submit_receipt("dfj")
|
||||
# convert 不自动创建 sample payment 金额
|
||||
```
|
||||
|
||||
- [ ] **Step 2: CRUD**
|
||||
|
||||
- 预签约主表 + sample_types 多行 + transfers
|
||||
- `convert_to_protocol`:
|
||||
- 创建 `biz_collection_protocol`(`source_presign_order_id`、母亲等到 party、样本类型生成 sample 行但**价格/缴费手填空或默认空方案**)
|
||||
- 预签约 `order_status=converted`,`converted_protocol_id=...`
|
||||
- **不**把意向金拆到 transfer
|
||||
- 正式协议 bundle 的 `presignDepositHint.confirmedAmount` = Σ 预签约 yjf 转账
|
||||
|
||||
- [ ] **Step 3: API**
|
||||
|
||||
```
|
||||
CRUD /presign-orders
|
||||
POST /presign-orders/{id}/convert
|
||||
意向金转账:submit/confirm/reject(可复用同一状态函数)
|
||||
GET 财务列表可在 payment-transfers 合并接口中用 source 区分,或
|
||||
GET /presign-transfers/receipt-confirm
|
||||
```
|
||||
|
||||
推荐:**财务列表单一聚合接口** `GET /finance-receipt-pending` 返回 formal+presign,避免前端两次合并。若求快,前端两次请求 merge 亦可(在 Task 7/9 注明)。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: add presign order CRUD, deposit transfers, and convert API
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 预签约前端 + 菜单
|
||||
|
||||
**Files:**
|
||||
- Create: `web-admin/src/api/presign-order.ts`
|
||||
- Create: `web-admin/src/views/cell-storage/presign-order/index.vue`
|
||||
- Create: `web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue`
|
||||
- Modify: `web-admin/src/router/modules/sqxp.ts`
|
||||
- Modify: `backend/app/api/menu.py`
|
||||
|
||||
- [ ] **Step 1: 路由**(细胞存储 children 内,采集协议单附近)
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: '50000000000000000000000000000020',
|
||||
name: 'CellStoragePresignOrder',
|
||||
path: 'presign-order',
|
||||
component: '/cell-storage/presign-order/index',
|
||||
meta: {
|
||||
title: '预签约订单',
|
||||
keepAlive: true,
|
||||
menuCode: 'cell-storage:presign-order:list'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`menu.py` 同步一项。
|
||||
|
||||
- [ ] **Step 2: 列表页**
|
||||
|
||||
对照 `unit/index.vue`:筛选(母亲姓名、协议编号、日期)+ 表格列对齐设计字段;到账金额用已确认合计。
|
||||
|
||||
- [ ] **Step 3: 表单弹窗**
|
||||
|
||||
- 年/月/日可用一个 `orderDate` 日期控件
|
||||
- 样本类型 `el-select` multiple
|
||||
- 整单 contractAmount、payMode
|
||||
- 意向金转账笔可增删;提交财务
|
||||
- 「转正式协议」按钮 → convert → 跳转或打开正式协议编辑并带意向金提示
|
||||
|
||||
- [ ] **Step 4: 财务页合并预签约待办**(补完 Task 7)
|
||||
|
||||
卡片 `source` 徽章:预签约 / 正式
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): add presign order menu and deposit finance flow
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: 适配收尾与验收
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue`
|
||||
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue`(只读展示 samplePayments + refundRecords + 意向金提示)
|
||||
- Modify: `web-admin/src/views/cell-storage/protocol-audit/index.vue`(若需入口)
|
||||
|
||||
- [ ] **Step 1: PaymentInfoTab**
|
||||
|
||||
version=2:按转账笔分列(dfj/jfqrz/yjf);去掉提交退费入口。
|
||||
version=1:保留旧逻辑分支。
|
||||
|
||||
- [ ] **Step 2: ViewDialog**
|
||||
|
||||
只读展示各样本方案、批次已缴/欠缴、转账状态 Tag、退费登记列表。
|
||||
|
||||
- [ ] **Step 3: 验收清单(手工)**
|
||||
|
||||
| # | 场景 | 期望 |
|
||||
|---|------|------|
|
||||
| 1 | 新建两样本协议,样本A分期6期、样本B趸交 | 两套方案独立保存 |
|
||||
| 2 | 趸交批次下录3笔并逐笔财务确认 | 3条待办;已缴累加正确 |
|
||||
| 3 | 总金额≠样本合计 | 可保存,有 warning |
|
||||
| 4 | 登记2条退费 | 财务页无退费 Tab;协议可见历史 |
|
||||
| 5 | 预签约多选类型+意向金确认+转正式 | 正式单见提示;样本金额手填 |
|
||||
| 6 | 旧 version=1 协议(若有) | 旧缴费 UI/API 仍可用 |
|
||||
|
||||
- [ ] **Step 4: 更新设计文档状态为「实现中/已实现(P0)」**
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat: finish P0 per-sample payment and presign order integration
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
- [ ] **Step 6:(可选)更新知识图谱**
|
||||
|
||||
```bash
|
||||
./scripts/update-graph.sh all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## P1 后续(本计划不实施,仅登记)
|
||||
|
||||
- 开票 `payment_transfer_id` 改造(设计 §5.9)
|
||||
- 旧协议迁移工具
|
||||
- `paymentSummary` / 质检费用摘要全面切换
|
||||
- OA 退费实例 id 回写 `workflow_instance_id`
|
||||
|
||||
---
|
||||
|
||||
## Spec coverage 自检
|
||||
|
||||
| 设计章节 | 对应 Task |
|
||||
|----------|-----------|
|
||||
| §3 菜单 | Task 7、9 |
|
||||
| §5 表结构 | Task 1 |
|
||||
| §6.1 转账状态机 | Task 2、3、8 |
|
||||
| §6.2 分改趸样本级 | Task 6 |
|
||||
| §6.4 退费登记 | Task 4、6 |
|
||||
| §7.1 预签约 UI | Task 9 |
|
||||
| §7.2 协议缴费 UI | Task 6 |
|
||||
| §7.3 财务页 | Task 7、9 |
|
||||
| §8 API | Task 3、4、8 |
|
||||
| §9 总金额提示 | Task 6 |
|
||||
| §10 新旧版本 | Task 5、10 |
|
||||
| §11 P0-a/b/c | Task 1–10 |
|
||||
| 开票改挂 / 迁移 | P1,未纳入 |
|
||||
|
||||
**Placeholder scan:** 无 TBD 实现步骤;开票 P1 已明确延期。
|
||||
|
||||
---
|
||||
|
||||
## Execution Handoff
|
||||
|
||||
Plan complete and saved to `docs/superpowers/plans/2026-10-03-per-sample-payment-presign.md`.
|
||||
|
||||
**两种执行方式:**
|
||||
|
||||
1. **Subagent-Driven(推荐)** — 每任务派生子代理,任务间复审,迭代快
|
||||
2. **Inline Execution** — 本会话按 executing-plans 连续执行并设检查点
|
||||
|
||||
你更倾向哪一种?
|
||||
@@ -0,0 +1,487 @@
|
||||
# 细胞存储三模块现状分析(改码参考)
|
||||
|
||||
**日期**:2026-10-03
|
||||
**状态**:现状梳理(非改造设计)
|
||||
**用途**:后续按新思路改代码前的权威对照基线;描述「当前代码实际如何工作」,而非「应当如何」。
|
||||
|
||||
**覆盖模块**:
|
||||
|
||||
| 菜单 | Hash 路由 | menuCode |
|
||||
|------|-----------|----------|
|
||||
| 采集协议单 | `/#/cell-storage/protocol-audit` | `biz:cellStorageProtocolAudit:list` |
|
||||
| 样本信息管理 | `/#/cell-storage/sample-info` | `cell-storage:sample-info:list` |
|
||||
| 缴退费财务确认及开票 | `/#/cell-storage/payment-confirm` | `biz:cellStoragePaymentConfirm:list` |
|
||||
|
||||
**需求权威**:`docs/中科兴皖需求V0.3.md` §2.2.6~2.2.9
|
||||
|
||||
**相关设计文档**(增量能力):
|
||||
|
||||
- `docs/superpowers/specs/2026-05-13-cell-storage-protocol-modules-design.md`(早期模块设计)
|
||||
- `docs/superpowers/specs/2026-05-19-protocol-donation-convert-design.md`(转科研)
|
||||
- `docs/superpowers/specs/2026-06-04-protocol-pay-mode-switch-design.md`(缴费模式切换,若存在)
|
||||
- `docs/superpowers/specs/2026-06-17-protocol-invoice-request-design.md`(开票)
|
||||
- `docs/superpowers/specs/2026-06-01-sample-qc-sms-notification-design.md`
|
||||
- `docs/superpowers/specs/2025-05-23-storage-entry-sms-notification-design.md`
|
||||
- OpenSpec:`openspec/changes/zkxw-engineering-standards/specs/api-conventions/spec.md`(细胞存储样本跨表关联键)
|
||||
|
||||
---
|
||||
|
||||
## 1. 总览:双并行业务架构
|
||||
|
||||
采集协议单是源头。创建后并行两条线:
|
||||
|
||||
```
|
||||
┌──────────────────── 细胞存储 ────────────────────┐
|
||||
│ 【采集协议单】 合同 + 客户 + 样本源头 + 缴费配置 │
|
||||
│ │ │
|
||||
│ ├──────────────┬─────────────────────────┤
|
||||
│ ▼ ▼ │
|
||||
│ 【样本信息管理】 【缴退费财务确认及开票】 │
|
||||
│ 只读运营台+短信 财务终审+开票完成 │
|
||||
│ │ ▲ │
|
||||
│ ▼ │ 业务侧发起 │
|
||||
│ 接收→质检→生产→入库 缴费信息 Tab / 满月待办 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 模块 | 角色 | 本质 |
|
||||
|------|------|------|
|
||||
| 采集协议单 | 市场/采集专员 | 主单据 CRUD、状态变更、缴费发起、开票申请入口 |
|
||||
| 样本信息管理 | 运营 | **只读查询台** + 线下短信提取/标记(不登记接收、不做质检入库操作) |
|
||||
| 缴退费财务确认及开票 | 财务 | 收款/退费确认 + 开票完成 |
|
||||
|
||||
**菜单演进**:样本相关 Tab、缴费确认、开票已从早期「单一协议管理页」拆出;隐藏路由仍保留 `protocol`(录入表格版)、`payment-info` 等兼容入口。
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据模型(以协议为中心)
|
||||
|
||||
```
|
||||
biz_collection_protocol (1)
|
||||
├── biz_protocol_party_customer (1) 当事人/客户
|
||||
├── biz_protocol_collection_info (1) 采集信息
|
||||
├── biz_protocol_sample (N)
|
||||
│ └── biz_protocol_sample_transport
|
||||
├── biz_protocol_payment (1)
|
||||
│ └── biz_protocol_payment_detail (N)
|
||||
├── biz_protocol_audit (N) 审核记录(不改协议状态)
|
||||
├── biz_protocol_storage_cert
|
||||
├── biz_protocol_fullmoon_todo
|
||||
├── biz_protocol_invoice_request (N)
|
||||
│ └── biz_protocol_invoice_request_item (N)
|
||||
└── biz_protocol_donation_convert (0..1) 与科研捐赠互转
|
||||
```
|
||||
|
||||
### 2.1 跨表关联硬规则(OpenSpec)
|
||||
|
||||
```
|
||||
关联键 = tenant_no + sample_code
|
||||
禁止用 collection_protocol_id / protocol_sample_id 做业务匹配
|
||||
```
|
||||
|
||||
原因:协议编辑可能重建样本行 `id`;协议可转科研;样本编码不变。接收防重、详情回显、质检/入库 JOIN 均按此规则。
|
||||
|
||||
### 2.2 协议主表关键字段
|
||||
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
| `protocol_no` / `protocol_no_id` | 协议号(可自动分配号池) |
|
||||
| `protocol_status` | `dsx` 待生效 / `sxz` 生效中 / `ygq` 已过期 / `yzf` 已作废 / `sdfq` 属地废弃 / `zky` 转科研 |
|
||||
| `protocol_attachments` / `consent_form_urls` / `health_survey_form_urls` | 附件 URL 逗号分隔 |
|
||||
| `void_type` | 作废类型(字典);作废时必填 |
|
||||
| `collector_user_id` | 采集专员 |
|
||||
| `effective_*` / `expire_*` / `void_*` / `to_research_*` | 各状态变更时间与描述 |
|
||||
|
||||
无效态 `yzf` / `sdfq`:下游接收、生产列表会过滤。
|
||||
|
||||
---
|
||||
|
||||
## 3. 【采集协议单】模块
|
||||
|
||||
### 3.1 前端结构
|
||||
|
||||
```
|
||||
web-admin/src/views/cell-storage/
|
||||
├── protocol/ # 隐藏录入列表
|
||||
│ ├── index.vue
|
||||
│ ├── ProtocolFormDialog.vue # 新增/编辑大表单(权威录入)
|
||||
│ ├── ProtocolViewDialog.vue
|
||||
│ ├── ProtocolAuditDialog.vue
|
||||
│ └── ProtocolStatusChangeDialog.vue
|
||||
└── protocol-audit/ # 主管理页
|
||||
├── index.vue # Pill Tab
|
||||
├── ProtocolAuditTab.vue
|
||||
├── PaymentInfoTab.vue # 业务侧缴费看板
|
||||
├── StorageCertTab.vue
|
||||
├── FullmoonTodoTab.vue # 满月保险 + 开票申请入口
|
||||
└── Sample*InfoTab.vue # 被 sample-info 复用
|
||||
```
|
||||
|
||||
API:`web-admin/src/api/collection-protocol.ts`
|
||||
|
||||
### 3.2 协议状态机
|
||||
|
||||
```
|
||||
创建 → [dsx] 待生效
|
||||
│ 协议生效
|
||||
▼
|
||||
[sxz] 生效中 ──┬── 过期 → [ygq]
|
||||
├── 作废 → [yzf](须 voidType)
|
||||
├── 属地废弃 → [sdfq]
|
||||
└── 转科研 → [zky](convert API,建捐赠单)
|
||||
```
|
||||
|
||||
- **审核独立**:只写 `biz_protocol_audit`(`shtg`/`shth`),不驱动 `protocol_status`。
|
||||
- **协议号**:空则 `allocate_next_biz_no`;已存在不可改。
|
||||
- **删除**:软删主单 + 样本 + 运输,释放样本编码占用。
|
||||
|
||||
### 3.3 表单保存顺序(API 硬约束)
|
||||
|
||||
`_persist_protocol_sub_resources`(`backend/app/api/collection_protocol.py`):
|
||||
|
||||
```
|
||||
1. party_customer(可 upsert biz_customer)
|
||||
2. collection_info
|
||||
3. payment + payment_details ← 必须在样本之前
|
||||
4. samples(含运输)
|
||||
5. storage_cert
|
||||
```
|
||||
|
||||
原因:缴费校验失败时不得已占用样本编码。创建失败补偿 `soft_delete_collection_protocol`。
|
||||
|
||||
### 3.4 ProtocolFormDialog 字段分区
|
||||
|
||||
| Tab | 主要字段 | payload 路径 |
|
||||
|-----|----------|--------------|
|
||||
| 基本信息 | 协议状态*、登记日、采集专员、备注、三类附件 | 主表字段 + attachments |
|
||||
| 采集信息 | 地区、医院、医护、销售、样本/母血采集时间 | `collectionInfo` |
|
||||
| 样本及运输 | 类型、编码、价格、年限、状态、特殊标记、运输 | `samples[]` + `transport` |
|
||||
| 客户信息 | 关联客户、母亲/父亲/新生儿全套 | `partyCustomer` |
|
||||
| 缴费信息 | 模式、合同额、分期参数、明细行 | `payment` + `paymentDetails` |
|
||||
|
||||
- `formRules` **仅强制** `protocolStatus`。
|
||||
- 组件:`sample-transport-form-section`(样本+运输权威 UI)。
|
||||
- 附件:卡片化立即上传(OpenSpec UI 附件约定以本弹窗为样例)。
|
||||
|
||||
**提交前前端校验顺序**:
|
||||
|
||||
1. el-form validate
|
||||
2. 缴费模式 ↔ 有效样本数(`djfq` 仅 2 样本)
|
||||
3. 新建时样本编码 `usedInProtocol` 占用拦截
|
||||
4. 趸交应缴合计 ≠ 合同额 → **仅 warning,不阻断**
|
||||
5. `syncFqPaymentDetailRowCount` → POST/PUT
|
||||
|
||||
### 3.5 缴费默认定价(样本数 × 模式)
|
||||
|
||||
`web-admin/src/utils/protocol-payment-presets.ts`:
|
||||
|
||||
| 有效样本数(≠ yzf) | 可选模式 | 合同金额示例 |
|
||||
|---------------------|----------|-------------|
|
||||
| 1 | `dj` / `fq` | 趸交 22880;分期 25880(6 期) |
|
||||
| 2 | `dj` / `djfq` / `fq` | 趸交 37880;趸交+分期 41000;分期 44000 |
|
||||
|
||||
### 3.6 缴费模式切换矩阵
|
||||
|
||||
前端:`protocol-pay-mode-switch.ts`
|
||||
后端:`protocol_pay_mode_switch.py`(`LOCKED_STATUSES` / `BLOCKING_STATUSES`)
|
||||
|
||||
| 从 → 到 | 条件 | 结果 |
|
||||
|---------|------|------|
|
||||
| 任意 | 存在进行中确认(前端还拦 `ytf`) | 阻断 |
|
||||
| `dj` → `fq`/`djfq` | 已有锁定明细 | 阻断 |
|
||||
| `dj` → `fq`/`djfq` | 全待缴 | 确认后按期数重建 |
|
||||
| `fq`/`djfq` → `dj` | 已全部缴清 | 阻断 |
|
||||
| `fq`/`djfq` → `dj` | 有已缴+未缴 | 保留 yjf,作废 dfj,新增剩余趸交 |
|
||||
| `fq` ↔ `djfq` | 已有锁定 | 阻断;否则重建 |
|
||||
|
||||
后端保存 P0:`jfqrz`/`tfqrz` 禁止保存;分期条数=期数;比例合计 100%;期数不少于已缴最大期次。
|
||||
**注意**:前端 `BLOCKING` 含 `ytf`,后端不含(已退费允许随协议保存只读明细)。
|
||||
|
||||
### 3.7 后端核心文件
|
||||
|
||||
| 路径 | 角色 |
|
||||
|------|------|
|
||||
| `backend/app/api/collection_protocol.py` | 协议/缴费/储存证/满月/开票 API |
|
||||
| `backend/app/crud/collection_protocol.py` | 核心 CRUD(约 3900+ 行) |
|
||||
| `backend/app/services/protocol_pay_mode_switch.py` | 缴费校验 |
|
||||
| `backend/app/crud/collection_protocol_no.py` | 协议号池 |
|
||||
| `backend/app/models/models.py` | ORM |
|
||||
|
||||
---
|
||||
|
||||
## 4. 【样本信息管理】模块
|
||||
|
||||
### 4.1 定位
|
||||
|
||||
**不是**样本录入页,**不是**接收/质检/入库操作台。
|
||||
|
||||
是协议样本的 **全链路查询 + 客户短信触达台**(组件复用自 `protocol-audit/`):
|
||||
|
||||
| Tab | 职责 | 短信 |
|
||||
|-----|------|------|
|
||||
| 样本采集信息 | 采集进度 | 无 |
|
||||
| 样本接收信息 | 接收结论 | 有 |
|
||||
| 样本质检信息 | 入库质检结论 | 有 |
|
||||
| 样本入库信息 | 入库/拒入库 | 有 |
|
||||
|
||||
深链:`?tab=sample-collection|sample-reception|sample-qc|storage-entry`
|
||||
旧路由 `sample-reception-info` / `storage-entry-info` 重定向到本页。
|
||||
|
||||
### 4.2 样本状态 `sample_status`
|
||||
|
||||
```
|
||||
(空) 未采集 → ycj 已采集 → jcz 检测中 → yrk 已入库
|
||||
任意 → yzf 已作废
|
||||
```
|
||||
|
||||
- 协议表单录入默认常为 `ycj`。
|
||||
- **接收登记不改** `sample_status`。
|
||||
- **入库**:`store_sample` 按 `sample_code` 回写 `yrk` + `storage_in_date`。
|
||||
- **取消入库**:若原为 `yrk` → 回退 `jcz`,清空 `storage_in_date`。
|
||||
|
||||
### 4.3 真实写库链路(本页只读展示)
|
||||
|
||||
接收结论 = `js` 时(`create_sample_reception`):
|
||||
|
||||
```
|
||||
写 biz_sample_reception + 检查项
|
||||
→ create_work_order_from_reception # 入库质检工单 dpz
|
||||
→ create_production_work_order_from_reception
|
||||
→ try_auto_dispatch_builtin_tasks… # 内置任务派发
|
||||
→ create_review_from_reception # 入库放行审核 drk
|
||||
```
|
||||
|
||||
结论 = `bj`:只写接收,不创建工单/审核。
|
||||
|
||||
防重:`tenant_no + reception_source + sample_code`。
|
||||
实验室入库编码:结论为 `js` 时必填且租户唯一。
|
||||
|
||||
### 4.4 质检结论派生
|
||||
|
||||
`backend/app/services/qc_work_order_conclusion.py`:
|
||||
|
||||
```
|
||||
任一任务 conclusion == btg → 工单 btg
|
||||
全部任务 conclusion == tg → 工单 tg
|
||||
否则 → ""(尚无结论)
|
||||
```
|
||||
|
||||
工单状态:`dpz` → `ypz` →(全部任务 ywc)`ywc` 并落库 `wo.conclusion`。
|
||||
`try_reopen_work_order`:回退 `ypz`,清空 conclusion。
|
||||
|
||||
质检 Tab 列表派生(`_qc_status_label`):
|
||||
|
||||
| 条件 | 码 |
|
||||
|------|-----|
|
||||
| 无工单等 | `dzj` 待质检 |
|
||||
| `ypz` | `zjz` 质检中 |
|
||||
| `ywc` + tg/btg | `tg` / `btg` |
|
||||
| `ywc` 结论未齐 | `zjz` |
|
||||
| 工单 `yzf` | `yzf` |
|
||||
|
||||
协议详情回显:`get_qc_info_map_for_samples` 细胞存储侧必须按 **sample_code** 关联(`trigger_source=reception`)。
|
||||
|
||||
### 4.5 入库双审
|
||||
|
||||
`submit_review`(production / qc 两侧):
|
||||
|
||||
- 任一方 `btg` → 聚合 `btg`
|
||||
- 双方 `tg` → 聚合 `tg` → `storage_transition` = `store` → `store_sample`
|
||||
- 已入库后结论变 `btg` → `unstore`
|
||||
|
||||
前置:生产与质检均通过且 `storage_status=drk` 才能入库。
|
||||
|
||||
### 4.6 短信规则
|
||||
|
||||
系统**不自动发送**。运营:提取文案 → 复制 → 线下发 → 标记已通知。
|
||||
|
||||
| Tab | 模板要点 |
|
||||
|-----|----------|
|
||||
| 接收 | 已于{日期}完成接收,结论:{接收/拒收} |
|
||||
| 质检 | 合格 / 未通过;未完成返回「暂无短信内容…」 |
|
||||
| 入库 | 已入库日期 / 审核未通过;待确认暂无内容 |
|
||||
|
||||
### 4.7 与缴退费关系
|
||||
|
||||
- **无直接 FK**;缴费挂协议不挂样本行。
|
||||
- 间接:有效样本数 → 默认定价 / 可选缴费模式。
|
||||
- 本页无缴费操作;查看协议弹窗内可见缴费。
|
||||
|
||||
### 4.8 代码锚点
|
||||
|
||||
| 路径 | 角色 |
|
||||
|------|------|
|
||||
| `web-admin/.../sample-info/index.vue` | 壳页 |
|
||||
| `web-admin/.../protocol-audit/Sample*InfoTab.vue` | 四 Tab |
|
||||
| `backend/app/api/protocol_sample_collection.py` | 采集列表 |
|
||||
| `backend/app/api/sample_sms_notification.py` 等 | 三短信 API |
|
||||
| `backend/app/crud/sample_reception.py` | 接收写库 |
|
||||
| `backend/app/crud/qc_work_order.py` | 质检工单 |
|
||||
| `backend/app/crud/storage_release_review.py` | 入库放行 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 【缴退费财务确认及开票】模块
|
||||
|
||||
### 5.1 三角色协作
|
||||
|
||||
```
|
||||
业务(缴费信息 / 满月待办) 财务(本模块)
|
||||
提交缴费确认 dfj→jfqrz → 确认收款 → yjf(或退回→dfj)
|
||||
提交退费确认 yjf→tfqrz → 确认退费 → ytf(或退回→yjf)
|
||||
发起开票申请 → dkp → 完成开票 → ykp(上传发票)
|
||||
```
|
||||
|
||||
前端:
|
||||
|
||||
- `payment-confirm/index.vue`(缴费确认)
|
||||
- `RefundConfirmTab.vue`
|
||||
- `InvoiceRequestTab.vue`
|
||||
- 上游:`PaymentInfoTab.vue`、`FullmoonTodoTab.vue`
|
||||
|
||||
### 5.2 缴费明细状态机(`pay_status`)
|
||||
|
||||
```
|
||||
dfj → jfqrz → yjf → tfqrz → ytf
|
||||
↑退回 ↑退回
|
||||
```
|
||||
|
||||
| 状态 | 业务可做 | 财务可做 | 关键写入 |
|
||||
|------|----------|----------|----------|
|
||||
| `dfj` | 编辑草稿、提交确认 | — | 转账方式/金额/时间/凭证 |
|
||||
| `jfqrz` | — | 确认/退回 | finance_* → yjf 或 → dfj |
|
||||
| `yjf` | 发起退费 | — | refund_* → tfqrz |
|
||||
| `tfqrz` | — | 确认/退回 | refund_finance_* → ytf 或 → yjf |
|
||||
| `ytf` | — | — | 终态 |
|
||||
|
||||
提交收款强制:转账方式非空 + `transfer_amount > 0`。
|
||||
提交退费强制:仅 `yjf` + `refund_amount > 0`。
|
||||
|
||||
**注意**:批量提交收款不跑单条草稿完整性校验(假定已是合格 `dfj`)。
|
||||
|
||||
### 5.3 开票(独立状态机,不改 `pay_status`)
|
||||
|
||||
```
|
||||
可开票 = pay_status=yjf AND 明细未在有效申请子表(dkp|ykp)
|
||||
金额 = Σ transfer_amount(快照 line_amount / invoice_amount)
|
||||
完成 = 仅 dkp + 发票附件必填 → ykp
|
||||
不做 = 驳回、冲红、改缴费状态、结构化发票号/日期
|
||||
```
|
||||
|
||||
- 多期可合并一张发票(1 主表 + N 子表)。
|
||||
- 已占用期不可再选;剩余期可另起申请。
|
||||
- 抬头六字段申请时 **选填**。
|
||||
|
||||
函数:`create_invoice_request` / `complete_invoice_request` / `list_invoice_eligible_payment_details`。
|
||||
|
||||
### 5.4 平行模块(勿混淆)
|
||||
|
||||
| 模块 | 差异 |
|
||||
|------|------|
|
||||
| 临床缴退费财务确认 | `biz_pilot_payment`;无开票 Tab |
|
||||
| 渠道服务订单财务确认 | 另一套流水状态机 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 协议转科研(`zky`)边界
|
||||
|
||||
入口:`research_donation.convert_from_storage`。
|
||||
|
||||
### 6.1 前置
|
||||
|
||||
- 源协议 `sxz`
|
||||
- 无有效 `biz_protocol_donation_convert`
|
||||
- 协议号非空
|
||||
|
||||
### 6.2 事务内拷贝
|
||||
|
||||
| 拷贝 | 不拷贝 |
|
||||
|------|--------|
|
||||
| 捐赠主单(同号 `donation_no`) | **缴费主表/明细** |
|
||||
| party / collection | 审核 / 开票 / 储存证 / 满月 |
|
||||
| samples(同 sample_code)+ transports | 接收/质检/入库审核记录 |
|
||||
|
||||
源协议 → `zky` + `to_research_*`;写关联表 `convert_type=storage_to_research`。
|
||||
|
||||
### 6.3 同码共存
|
||||
|
||||
配对后两边可暂共用 `sample_code`;保存唯一性对配对 ID **豁免**(`get_partner_sample_codes`)。
|
||||
之后两边可各自改号,**不自动同步**;关联表 `biz_no` 只留转移快照。
|
||||
|
||||
产品含义:转科研是合同形态切换,**不是财务结算迁移**;退费/开票仍对原协议缴费明细。
|
||||
|
||||
---
|
||||
|
||||
## 7. 端到端写库总图
|
||||
|
||||
```
|
||||
协议保存
|
||||
客户 → 采集 →【缴费校验+明细】→ 样本编码占用 → 储存证
|
||||
│ │
|
||||
▼ ▼
|
||||
缴费明细状态机 sample_code
|
||||
dfj→jfqrz→yjf→… │
|
||||
│ ▼
|
||||
│ 接收(js) → QC + 生产 + 入库审核(drk)
|
||||
│ │
|
||||
▼ ▼ 双审 tg
|
||||
财务确认 / 开票(dkp→ykp) store → sample_status=yrk
|
||||
|
||||
转科研(sxz→zky):新捐赠同号同码;缴费不跟走
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 实现时注意点(改码易踩坑)
|
||||
|
||||
1. **跨表只认 `tenant_no + sample_code`**,勿用样本行 id 做业务匹配。
|
||||
2. **缴费先于样本**保存;创建失败要补偿软删。
|
||||
3. **审核不改协议状态**;财务确认不改协议状态;开票不改 `pay_status`。
|
||||
4. **前端/后端缴费阻断集合不完全一致**(`ytf`)。
|
||||
5. **批量提交收款**校验弱于单条。
|
||||
6. **转科研不迁缴费**;样本下游历史仍按编码可查。
|
||||
7. **管理端色值**用 Fresh Mint(`#10b981`),勿混小程序三墙绿。
|
||||
8. 平行「缴退费财务确认」菜单(临床/渠道)表模型不同。
|
||||
|
||||
---
|
||||
|
||||
## 9. 关键代码地图(速查)
|
||||
|
||||
### 前端
|
||||
|
||||
| 模块 | 路径 |
|
||||
|------|------|
|
||||
| 协议主页 | `web-admin/src/views/cell-storage/protocol-audit/` |
|
||||
| 协议表单 | `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` |
|
||||
| 样本信息 | `web-admin/src/views/cell-storage/sample-info/index.vue` |
|
||||
| 财务确认 | `web-admin/src/views/cell-storage/payment-confirm/` |
|
||||
| API | `web-admin/src/api/collection-protocol.ts` 等 |
|
||||
| 缴费工具 | `web-admin/src/utils/protocol-payment-*.ts`、`protocol-pay-mode-switch.ts` |
|
||||
| 路由 | `web-admin/src/router/modules/sqxp.ts` |
|
||||
|
||||
### 后端
|
||||
|
||||
| 模块 | 路径 |
|
||||
|------|------|
|
||||
| 协议 API/CRUD | `backend/app/api/collection_protocol.py`、`crud/collection_protocol.py` |
|
||||
| 样本采集列表 | `api/protocol_sample_collection.py`、`crud/protocol_sample_collection.py` |
|
||||
| 接收 | `crud/sample_reception.py` |
|
||||
| 质检 | `crud/qc_work_order.py`、`services/qc_work_order_conclusion.py` |
|
||||
| 入库 | `crud/storage_release_review.py` |
|
||||
| 短信 | `crud/sample_sms_notification.py`、`qc_sms_notification.py`、`storage_entry_sms_notification.py` |
|
||||
| 转科研 | `crud/research_donation.py`(`convert_from_storage`)、`crud/protocol_donation_convert.py` |
|
||||
| 缴费模式 | `services/protocol_pay_mode_switch.py` |
|
||||
|
||||
---
|
||||
|
||||
## 10. 后续改码建议用法
|
||||
|
||||
1. 先在本文确认「现状行为」与「要改的边界」。
|
||||
2. 若行为变更跨状态机/表结构,另开 OpenSpec change 或 `docs/superpowers/specs/YYYY-MM-DD-*-design.md`。
|
||||
3. 改样本下游关联时,对照 §2.1 与 OpenSpec api-conventions。
|
||||
4. 改缴费/开票时,对照 §5 状态矩阵与「明确不做」列表,避免无意引入驳回/冲红等本期未实现能力。
|
||||
5. 改协议表单字段时,同步检查:Create/Update Body、`_row_to_dict`、前端 `formData`(见 `docs/dev-field-consistency.md`)。
|
||||
|
||||
---
|
||||
|
||||
**文档维护**:若实现偏离本文描述,请同步更新本节或标注「已失效段落」,避免后续改码基于过时基线。
|
||||
@@ -0,0 +1,481 @@
|
||||
# 按样本缴费 + 预签约订单 + 意向金 — 设计说明(方案 2)
|
||||
|
||||
**日期**:2026-10-03
|
||||
**状态**:已实现(P0)· 实现计划见 `docs/superpowers/plans/2026-10-03-per-sample-payment-presign.md`
|
||||
**方案**:方案 2 — 三层缴费(样本方案 → 应缴批次 → 转账笔)+ 预签约独立模块
|
||||
|
||||
**关联现状分析**:`docs/superpowers/specs/2026-10-03-cell-storage-protocol-sample-payment-analysis.md`
|
||||
|
||||
**关联既有设计**(将被本设计部分取代或调整):
|
||||
|
||||
- `2026-06-04-protocol-pay-mode-switch-design.md`(模式切换下沉到样本级)
|
||||
- `2026-06-17-protocol-invoice-request-design.md`(开票对象改为转账笔)
|
||||
- 采集协议单财务退费确认流程(**本期取消**,改为协议级登记 + 工作流审批)
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 问题
|
||||
|
||||
现状:1 协议 → 1 套缴费主表 → N 条明细(期次与转账揉在一行);多样本时按**金额之和**做趸交/分期。
|
||||
实际业务:每个样本各自趸交/分期;一期/趸交计划下可有**多笔转账**;财务按**笔**确认;退费走工作流,协议侧只需登记。
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
1. 正式协议:**每样本一套缴费方案**(合同金额、缴费方式、分期参数)。
|
||||
2. 缴费分层:**应缴批次** + **转账笔**;财务确认最小单位 = 转账笔。
|
||||
3. 协议级保留**可手填总金额**;与样本合同金额合计不一致时仅提示。
|
||||
4. 新增**预签约订单**:录入意向金;按笔财务确认;转正式协议时仅提示意向金金额。
|
||||
5. **退费**:协议级登记(金额/备注/时间/附件),审批在工作流;下线协议模块内退费财务确认。
|
||||
|
||||
### 1.3 非目标(本期不做)
|
||||
|
||||
- 转正式单时自动冲抵/拆分意向金到样本。
|
||||
- 强制协议总金额 = 样本合同金额之和。
|
||||
- 开票驳回、冲红。
|
||||
- 历史协议自动拆分迁移(见 §10;默认新单新模型,旧单兼容策略另定)。
|
||||
- 新建独立「退费工作流」菜单(假设使用既有 OA;本模块只登记)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 已确认产品决策
|
||||
|
||||
| # | 议题 | 决策 |
|
||||
|---|------|------|
|
||||
| D1 | 财务确认粒度 | **每一笔转账**(转 3 次确认 3 次) |
|
||||
| D2 | 意向金入口 | **预签约订单**;字段含到账金额、刷卡时间等 |
|
||||
| D3 | 预签约样本 | **多样本类型多选** |
|
||||
| D4 | 预签约金额/方式 | **整单一套**合同总额 + 缴费方式 |
|
||||
| D5 | 意向金财务 | 与正式缴费相同:登记 → 提交财务 → **按笔确认** |
|
||||
| D6 | 转正式协议 | 缴费区上方**提示意向金数量**;正式金额**全手填**,不自动冲抵 |
|
||||
| D7 | 协议总金额 | **可手填**;与样本合计不一致仅提示 |
|
||||
| D8 | 退费 | **不走**采集协议单财务退费确认;工作流审批;协议侧**协议级登记**(可多条历史) |
|
||||
| D9 | 菜单 | 新增 1 个「预签约订单」;改造「采集协议单」「缴退费财务确认及开票」 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 菜单与模块范围
|
||||
|
||||
### 3.1 新增菜单(1)
|
||||
|
||||
| 菜单 | 建议 path | 归属 |
|
||||
|------|-----------|------|
|
||||
| 预签约订单 | `/#/cell-storage/presign-order` | 细胞存储,与采集协议单同级 |
|
||||
|
||||
### 3.2 改造菜单(2)
|
||||
|
||||
| 菜单 | 改造要点 |
|
||||
|------|----------|
|
||||
| 采集协议单 | 按样本缴费 UI;协议总金额;退费登记;转单意向金提示;缴费 Tab 适配 |
|
||||
| 缴退费财务确认及开票 | 按转账笔待办(正式 + 预签约意向金);**移除/隐藏退费确认 Tab**;开票挂转账笔;菜单文案建议改为「缴费财务确认及开票」 |
|
||||
|
||||
### 3.3 隐藏/废弃
|
||||
|
||||
| 项 | 处理 |
|
||||
|----|------|
|
||||
| 缴费信息(已隐藏) | 随新模型改造,仍可隐藏 |
|
||||
| 退费确认(财务)(已隐藏) | 废弃,不再使用 |
|
||||
| 支付明细上的 `tfqrz`/`ytf` 退费状态机 | 新模型不再使用;旧数据只读兼容 |
|
||||
|
||||
### 3.4 连带
|
||||
|
||||
| 模块 | 说明 |
|
||||
|------|------|
|
||||
| 样本信息管理 | 查看协议弹窗缴费展示适配 |
|
||||
| OA 工作流 | 退费审批配置(若已有则对接登记;本设计不规定流程节点) |
|
||||
| 质检/生产费用摘要 | `paymentSummary` 改为按样本/转账笔汇总 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 领域模型
|
||||
|
||||
### 4.1 总览
|
||||
|
||||
```
|
||||
biz_presign_order(预签约订单)
|
||||
├── 样本类型多选(子表或逗号/JSON)
|
||||
└── biz_presign_transfer(意向金转账笔)→ 财务按笔确认
|
||||
│
|
||||
│ 转正式协议(复制客户/样本类型等;缴费手填)
|
||||
▼
|
||||
biz_collection_protocol
|
||||
├── protocol_total_amount(手填总金额)
|
||||
├── source_presign_order_id(可空)
|
||||
├── biz_protocol_sample (N)
|
||||
│ └── biz_protocol_sample_payment(每样本 1 套方案)
|
||||
│ ├── pay_mode / contract_amount / 分期参数
|
||||
│ └── biz_protocol_payment_schedule(应缴批次)
|
||||
│ └── biz_protocol_payment_transfer(转账笔)→ 财务按笔确认
|
||||
├── biz_protocol_refund_record(协议级退费登记,N)
|
||||
└── biz_protocol_invoice_request
|
||||
└── item → transfer_id(改挂转账笔)
|
||||
```
|
||||
|
||||
### 4.2 与旧模型关系
|
||||
|
||||
| 旧表 | 新模型 |
|
||||
|------|--------|
|
||||
| `biz_protocol_payment`(1/协议) | 改为 **每样本** `biz_protocol_sample_payment`(或扩展旧表加 `sample_code` 且 1 协议 N 行) |
|
||||
| `biz_protocol_payment_detail`(期+转账合一) | 拆为 **schedule(批次)** + **transfer(转账笔)** |
|
||||
| detail 上退费字段/状态 | 废弃;改 `biz_protocol_refund_record` |
|
||||
|
||||
**实现选型(推荐)**:
|
||||
|
||||
- **新建** `biz_protocol_sample_payment` / `biz_protocol_payment_schedule` / `biz_protocol_payment_transfer` / `biz_presign_order` / `biz_presign_transfer` / `biz_protocol_refund_record`。
|
||||
- 旧 `biz_protocol_payment` + `detail`:**保留只读**服务历史协议(`payment_model_version=1`);新协议 `payment_model_version=2`。
|
||||
|
||||
(若产品要求「旧单也改成按样本」,另开迁移专项;见 §10。)
|
||||
|
||||
### 4.3 样本关联键
|
||||
|
||||
缴费方案、批次、转账笔关联样本时:
|
||||
|
||||
- **持久化**:同时存 `protocol_sample_id`(创建时)+ **`sample_code`(业务关联主键)**。
|
||||
- 列表/财务/开票 JOIN:**优先 `tenant_no + sample_code`**(与 OpenSpec 一致)。
|
||||
- 协议编辑重建样本行 id 后,按 `sample_code` 回绑,避免断链。
|
||||
|
||||
---
|
||||
|
||||
## 5. 表结构设计
|
||||
|
||||
### 5.1 预签约主表 `biz_presign_order`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | VARCHAR(64) | PK |
|
||||
| order_date | CHAR(8) | 业务日期 YYYYMMDD(对应年/月/日) |
|
||||
| protocol_no | VARCHAR(64) | 协议编号(预占或转单后回写,规则实现时定) |
|
||||
| mother_name | VARCHAR(64) | 母亲姓名 |
|
||||
| expected_delivery_date | CHAR(8) | 预产期 |
|
||||
| region_id / hospital_id | VARCHAR(64) | 采集地区/医院 |
|
||||
| sample_collect_time | DATETIME | 采集时间 |
|
||||
| pay_mode | VARCHAR(8) | 整单初步缴费方式 `dj`/`fq`/… |
|
||||
| contract_amount | DECIMAL(18,2) | 整单合同金额 |
|
||||
| sales_user_id | VARCHAR(64) | 市场专员 |
|
||||
| region_manager_user_id | VARCHAR(64) | 地区经理 |
|
||||
| remark | VARCHAR(512) | 备注 |
|
||||
| order_status | VARCHAR(16) | 如 `draft`/`active`/`converted`/`void`(最终码表实现定) |
|
||||
| converted_protocol_id | VARCHAR(64) | 转成的正式协议 id,可空 |
|
||||
| 公共字段 | | tenant_no, create_*, update_*, del_flag, … |
|
||||
|
||||
列表「到账金额」= Σ 已财务确认的意向金转账笔;「刷卡时间」可取最近一笔确认时间或业务填写的代表时间(实现时定展示规则)。
|
||||
|
||||
### 5.2 预签约样本类型 `biz_presign_order_sample_type`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| id | PK |
|
||||
| presign_order_id | 预签约 id |
|
||||
| sample_type_id | 样本类型 |
|
||||
| order_num | 排序 |
|
||||
|
||||
(多选几个类型就几行。)
|
||||
|
||||
### 5.3 预签约转账笔 `biz_presign_transfer`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| id | PK |
|
||||
| presign_order_id | |
|
||||
| transfer_amount | 缴费金额 |
|
||||
| transfer_time | 刷卡/转账时间 |
|
||||
| transfer_type | 缴费方式(微信/银行等字典) |
|
||||
| transfer_attachments | 附件 |
|
||||
| remark | |
|
||||
| pay_status | 同正式转账笔:`dfj`/`jfqrz`/`yjf`(**无退费态**) |
|
||||
| finance_confirm_attachments / finance_remark | 财务确认 |
|
||||
| 公共字段 | |
|
||||
|
||||
### 5.4 协议主表增量 `biz_collection_protocol`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| protocol_total_amount | 手填协议总金额 |
|
||||
| source_presign_order_id | 来源预签约,可空 |
|
||||
| payment_model_version | `1` 旧 / `2` 新;默认新单 = 2 |
|
||||
|
||||
### 5.5 样本缴费方案 `biz_protocol_sample_payment`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| id | PK |
|
||||
| collection_protocol_id | |
|
||||
| protocol_sample_id | 可空兜底 |
|
||||
| sample_code | **业务关联键** |
|
||||
| pay_mode | `dj` 趸交 / `fq` 分期 /(分改趸为操作结果,不必新枚举或增加 `fgd` 仅作标记) |
|
||||
| contract_amount | 该样本合同金额 |
|
||||
| installment_count / interval / start_date / advance_days / ratios 或 amounts | 分期参数 |
|
||||
| lump_sum_deadline | 趸交截止 |
|
||||
| 公共字段 | |
|
||||
|
||||
约束:同一协议下同一 `sample_code` 至多 1 条有效方案(`del_flag=0`)。
|
||||
|
||||
### 5.6 应缴批次 `biz_protocol_payment_schedule`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| id | PK |
|
||||
| sample_payment_id | 方案 id |
|
||||
| collection_protocol_id | 冗余便于列表 |
|
||||
| sample_code | 冗余 |
|
||||
| schedule_no | 期次:趸交可为 1 或 null;分期 1..N |
|
||||
| due_amount | 应缴金额 |
|
||||
| due_date | 应缴日期 YYYYMMDD |
|
||||
| schedule_status | 可选:`open`/`cleared`/`void`(或纯计算) |
|
||||
| remark | |
|
||||
| 公共字段 | |
|
||||
|
||||
**已缴金额** = Σ 下属转账笔中 `pay_status=yjf` 的 `transfer_amount`
|
||||
**欠缴金额** = `due_amount − 已缴`(展示用,可不落库)
|
||||
|
||||
### 5.7 转账笔 `biz_protocol_payment_transfer`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| id | PK |
|
||||
| schedule_id | 所属批次 |
|
||||
| sample_payment_id / collection_protocol_id / sample_code | 冗余 |
|
||||
| transfer_amount | |
|
||||
| transfer_time | |
|
||||
| transfer_type | |
|
||||
| transfer_attachments | |
|
||||
| remark | |
|
||||
| pay_status | `dfj` → `jfqrz` → `yjf` |
|
||||
| finance_confirm_attachments / finance_remark | |
|
||||
| 公共字段 | |
|
||||
|
||||
**说明**:退费相关字段**不放**本表。
|
||||
|
||||
### 5.8 协议退费登记 `biz_protocol_refund_record`
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| id | PK |
|
||||
| collection_protocol_id | |
|
||||
| refund_amount | |
|
||||
| refund_time | |
|
||||
| refund_remark | |
|
||||
| refund_attachments | |
|
||||
| workflow_instance_id | 可选,关联 OA 实例 |
|
||||
| 公共字段 | |
|
||||
|
||||
不绑样本、不绑转账笔;一协议可多条。
|
||||
|
||||
### 5.9 开票子表调整
|
||||
|
||||
`biz_protocol_invoice_request_item`:
|
||||
|
||||
- 新字段 `payment_transfer_id`(正式转账笔)。
|
||||
- 可开票条件:转账笔 `pay_status=yjf` 且未被有效开票占用。
|
||||
- 金额快照 = 转账笔 `transfer_amount`。
|
||||
- 旧 `payment_detail_id`:仅服务 `payment_model_version=1` 历史单。
|
||||
|
||||
---
|
||||
|
||||
## 6. 状态机
|
||||
|
||||
### 6.1 转账笔(正式 + 预签约意向金)
|
||||
|
||||
```
|
||||
dfj(待缴费/待提交)
|
||||
│ 业务提交财务确认
|
||||
▼
|
||||
jfqrz(缴费确认中)
|
||||
│ 财务确认 → yjf
|
||||
│ 财务退回 → dfj
|
||||
▼
|
||||
yjf(已缴费)
|
||||
```
|
||||
|
||||
- **无** `tfqrz` / `ytf`(新模型)。
|
||||
- 仅 `dfj` 可编辑转账草稿;仅 `dfj` 可提交;仅 `jfqrz` 可确认/退回。
|
||||
|
||||
### 6.2 缴费方式(样本级)
|
||||
|
||||
| 模式 | 批次 | 转账 |
|
||||
|------|------|------|
|
||||
| 趸交 `dj` | 通常 1 个应缴批次(应缴=样本合同额) | 批次下可多笔 |
|
||||
| 分期 `fq` | N 个批次(常 6 期) | 每批次可多笔 |
|
||||
| 分改趸 | 操作:保留已产生 yjf 的批次;作废未清批次;新建 1 个剩余应缴批次 | 同趸交 |
|
||||
|
||||
规则对齐旧「模式切换」设计,但作用域改为**单样本方案**。存在 `jfqrz` 时禁止切换。
|
||||
|
||||
### 6.3 预签约订单状态(建议)
|
||||
|
||||
| 状态 | 含义 |
|
||||
|------|------|
|
||||
| draft/active | 可编辑、可录意向金 |
|
||||
| converted | 已转正式协议 |
|
||||
| void | 作废 |
|
||||
|
||||
转正式:生成/关联 `biz_collection_protocol`,写入 `source_presign_order_id`;**不**自动生成样本缴费金额与转账。
|
||||
|
||||
### 6.4 退费
|
||||
|
||||
- 工作流:系统外/OA 完成审批。
|
||||
- 协议:CRUD `biz_protocol_refund_record` 登记结果。
|
||||
- **不改变**转账笔 `pay_status`。
|
||||
|
||||
---
|
||||
|
||||
## 7. 前端页面设计
|
||||
|
||||
### 7.1 预签约订单(新)
|
||||
|
||||
- 模式:标准列表(对照 `unit` / 复杂列表按需)+ 新增/编辑弹窗。
|
||||
- 列表列:对齐用户提供表头(序号、年/月/日、协议编号、母亲姓名、样本类型汇总、地区医院、缴费方式、采集时间、合同金额、到账金额、刷卡时间、预产期、市场专员、地区经理、备注)。
|
||||
- 表单:样本类型多选;整单合同额/缴费方式;意向金转账笔可编辑列表(多笔)。
|
||||
- 操作:提交财务确认、查看财务状态、**转正式协议**。
|
||||
|
||||
### 7.2 采集协议单 — 缴费区(改造)
|
||||
|
||||
```
|
||||
[提示条] 来源预签约意向金已确认合计:¥xxx(若有)
|
||||
[协议总金额] 手填 [样本合同合计] 只读汇总 不一致时 warning
|
||||
|
||||
Tab 或折叠:样本 A | 样本 B | …
|
||||
- 缴费方式、合同金额、分期参数
|
||||
- 应缴批次表(应缴、应缴日、已缴、欠缴)
|
||||
- 展开批次 → 转账笔列表(提交财务)
|
||||
```
|
||||
|
||||
- 分改趸:样本方案工具按钮,确认后重建未清批次。
|
||||
- **退费登记**:独立区块/弹窗,列表展示历史登记;新增金额/时间/备注/附件。
|
||||
|
||||
### 7.3 缴费财务确认及开票(改造)
|
||||
|
||||
| Tab | 内容 |
|
||||
|-----|------|
|
||||
| 缴费确认 | 待办 = 所有 `jfqrz` 转账笔(正式 + 预签约);已办 = `yjf` |
|
||||
| ~~退费确认~~ | **删除或隐藏** |
|
||||
| 开票申请 | 可选 `yjf` 转账笔;逻辑同前,对象改 transfer |
|
||||
|
||||
卡片需展示:协议号、母亲、样本编码(正式)、来源(正式/预签约)、金额、时间。
|
||||
|
||||
### 7.4 UI 规范
|
||||
|
||||
- 遵循 Fresh Mint / `docs/web-admin-ui-spec-fresh-mint.md`。
|
||||
- 弹窗 `dialog-form-layout`、`label-position=top`。
|
||||
- 按钮带图标;状态用 `el-tag`。
|
||||
- 模式索引:预签约列表偏 P01;协议缴费改造对照既有 protocol 页。
|
||||
|
||||
---
|
||||
|
||||
## 8. 后端 API 草案
|
||||
|
||||
前缀:`/web/admin`;租户隔离 + 数据权限。
|
||||
|
||||
### 8.1 预签约
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET/POST | `/presign-orders` | 列表/创建 |
|
||||
| GET/PUT/DELETE | `/presign-orders/{id}` | 详情/更新/软删 |
|
||||
| POST | `/presign-orders/{id}/convert` | 转正式协议 |
|
||||
| POST | `/presign-transfers/{id}/submit-receipt-finance` | 提交确认 |
|
||||
| POST | `/presign-transfers/{id}/receipt-confirm` / `receipt-reject` | 财务确认/退回 |
|
||||
|
||||
### 8.2 正式样本缴费
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| PUT | `/collection-protocols/{id}/sample-payments` | 批量保存各样本方案+批次(或分资源) |
|
||||
| POST | `/payment-transfers/{id}/submit-receipt-finance` | |
|
||||
| GET | `/payment-transfers/receipt-confirm` 等 | 财务列表(可与预签约合并查询层) |
|
||||
| POST | `/payment-transfers/{id}/receipt-confirm` / `reject` | |
|
||||
|
||||
### 8.3 退费登记
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET/POST | `/collection-protocols/{id}/refund-records` | |
|
||||
| PUT/DELETE | `/protocol-refund-records/{id}` | |
|
||||
|
||||
### 8.4 开票
|
||||
|
||||
- eligible 改为返回可开票 **transfer** 列表。
|
||||
- create item 写 `payment_transfer_id`。
|
||||
|
||||
### 8.5 废弃 API(新单不再调用)
|
||||
|
||||
- `submit-refund-finance` / `refund-confirm` / `refund-reject`(可保留供旧版协议 `payment_model_version=1`)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 定价与校验
|
||||
|
||||
### 9.1 默认定价
|
||||
|
||||
- 废弃「1/2 样本共享一套 `djfq` 总价」作为唯一方案。
|
||||
- 改为:**单样本类型价目**(或沿用原单样本价)写入该样本 `contract_amount`;双样本 = 两套方案。
|
||||
- 协议 `protocol_total_amount` 默认可带出样本合计,允许用户改。
|
||||
|
||||
### 9.2 保存校验
|
||||
|
||||
| 规则 | 级别 |
|
||||
|------|------|
|
||||
| 样本方案存在 `jfqrz` 时禁止改模式/重生成批次 | 阻断 |
|
||||
| 分期批次数 = 期数 | 阻断 |
|
||||
| 批次应缴合计与样本合同金额 | 建议提示,是否强制实现时定(默认提示) |
|
||||
| 协议总金额 ≠ Σ 样本合同金额 | **仅提示**(D7) |
|
||||
| 转账提交:金额>0、方式必填 | 阻断 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 历史数据策略
|
||||
|
||||
**推荐默认(P0)**:
|
||||
|
||||
- 新创建协议 / 预签约转正:`payment_model_version=2`。
|
||||
- 旧协议 `version=1`:继续走旧表与旧财务接口;前端按版本切换 UI。
|
||||
- 不做自动「总账拆到样本」。
|
||||
|
||||
**可选 P1**:人工/脚本迁移工具(按规则挂到第一样本或均分)— 需单独评审,不阻塞 P0。
|
||||
|
||||
---
|
||||
|
||||
## 11. 实施分期建议
|
||||
|
||||
| 阶段 | 内容 |
|
||||
|------|------|
|
||||
| P0-a | 表结构 + 样本缴费方案/批次/转账笔 CRUD + 协议总金额 + 按笔财务确认(正式) |
|
||||
| P0-b | 采集协议单缴费 UI 改造;财务确认页改造;下线退费确认 Tab;退费登记 |
|
||||
| P0-c | 预签约订单模块 + 意向金按笔确认 + 转正式 + 意向金提示 |
|
||||
| P1 | 开票改挂 transfer;旧单迁移工具;菜单文案「缴费财务确认及开票」 |
|
||||
| P2 | 费用摘要/查看弹窗等全量适配;文档与 OpenSpec 归档 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 风险与依赖
|
||||
|
||||
1. **协议编辑重建样本 id**:必须靠 `sample_code` 回绑缴费方案。
|
||||
2. **财务待办量增加**:按笔后卡片变多,需分页与筛选(协议号/母亲/样本编码/来源)。
|
||||
3. **开票**:与旧 detail 开票并存一期,注意双写/双读分支。
|
||||
4. **OA 退费**:本模块只登记;若工作流未就绪,登记仍可先做。
|
||||
5. **权限**:预签约、转账确认、退费登记需独立或复用 menuCode。
|
||||
|
||||
---
|
||||
|
||||
## 13. 验收要点(摘要)
|
||||
|
||||
1. 两样本协议可分别设趸交/分期,互不影响。
|
||||
2. 同一批次下登记 3 笔转账 → 财务出现 3 条待确认 → 确认后批次已缴累加正确。
|
||||
3. 协议总金额与样本合计不符时能保存且有提示。
|
||||
4. 预签约多选样本类型;意向金多笔确认;转正式后缴费区显示意向金提示且金额不自动写入样本。
|
||||
5. 协议可登记多条退费;财务页无退费确认待办(新单)。
|
||||
6. 旧版协议(若保留)仍可按旧流程处理收款。
|
||||
|
||||
---
|
||||
|
||||
## 14. 文档自检
|
||||
|
||||
- [x] 无「TBD」占位关键决策(实施码表细节已标明「实现时定」)
|
||||
- [x] 与 D1–D9 无矛盾
|
||||
- [x] 退费与按笔收款边界清晰
|
||||
- [x] 菜单范围与先前确认一致(+1 预签约,改 2 个既有)
|
||||
- [x] 未纳入自动冲抵意向金、强制总金额相等
|
||||
|
||||
---
|
||||
|
||||
**请审阅本文**。确认后可进入 `writing-plans` 拆实现任务;若需修改决策,直接标注章节号即可。
|
||||
@@ -48,6 +48,20 @@ export function fetchSubmitPaymentTransferFinance(transferId: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchPaymentTransferList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
payStatus?: string
|
||||
keyword?: string
|
||||
installmentDueDateStart?: string
|
||||
installmentDueDateEnd?: string
|
||||
}) {
|
||||
return request.get<PageList<Record<string, unknown>>>({
|
||||
url: '/web/admin/payment-transfers',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchPaymentTransferReceiptConfirmList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
|
||||
@@ -87,7 +87,12 @@
|
||||
size="small"
|
||||
class="payment-card__status"
|
||||
>
|
||||
{{ protocolStatusDisplayLabel(String(row.protocolStatus || ''), String(row.voidType || '')) }}
|
||||
{{
|
||||
protocolStatusDisplayLabel(
|
||||
String(row.protocolStatus || ''),
|
||||
String(row.voidType || '')
|
||||
)
|
||||
}}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,13 +163,7 @@
|
||||
>
|
||||
<el-icon><Check /></el-icon>缴费确认
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="col.key === 'yjf'"
|
||||
class="btn-op-item primary"
|
||||
@click.stop="openRefundFinanceDialog(row)"
|
||||
>
|
||||
<el-icon><Check /></el-icon>退费确认
|
||||
</el-button>
|
||||
<!-- 新模型退费改协议级登记,看板不再提交退费确认 -->
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -260,12 +259,7 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="receiptForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="选填"
|
||||
/>
|
||||
<el-input v-model="receiptForm.remark" type="textarea" :rows="3" placeholder="选填" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上传凭证" class="form-col-span-2">
|
||||
<div
|
||||
@@ -304,11 +298,7 @@
|
||||
</div>
|
||||
<div class="attachment-footer">
|
||||
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
|
||||
<el-icon
|
||||
class="action-btn"
|
||||
title="删除"
|
||||
@click="removeReceiptAttachment(fIdx)"
|
||||
>
|
||||
<el-icon class="action-btn" title="删除" @click="removeReceiptAttachment(fIdx)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
@@ -342,108 +332,6 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="refundDialogVisible"
|
||||
title="退费确认"
|
||||
width="640px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="refundFormRef"
|
||||
:model="refundForm"
|
||||
:rules="refundFormRules"
|
||||
label-position="top"
|
||||
class="biz-dialog-form"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="退费金额(元)" prop="refundAmount" class="form-col-span-2">
|
||||
<AmountInput
|
||||
v-model="refundForm.refundAmount"
|
||||
:min="0.01"
|
||||
:step="100"
|
||||
placeholder="请输入退费金额"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="退费备注" prop="refundRemark" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="refundForm.refundRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="选填"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="退费附件" class="form-col-span-2">
|
||||
<div
|
||||
class="attachment-upload"
|
||||
:class="{ 'is-dragover': isRefundAttachDragover }"
|
||||
v-bind="refundAttachDropHandlers"
|
||||
>
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||||
@change="handleRefundAttachmentChange"
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="22" color="#047857"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
v-for="(file, fIdx) in refundForm.attachments"
|
||||
:key="file.id || fIdx"
|
||||
class="attachment-item"
|
||||
:class="{ 'is-uploading': file._uploading }"
|
||||
>
|
||||
<div class="attachment-preview">
|
||||
<template v-if="file.fileUrl && isImageUrl(file.fileUrl)">
|
||||
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="attachment-preview__icon attachment-preview__icon--doc">
|
||||
<el-icon :size="28"><Document /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="attachment-footer">
|
||||
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
|
||||
<el-icon class="action-btn" title="删除" @click="removeRefundAttachment(fIdx)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="form-tip">
|
||||
支持拖拽或点击上传图片、文档等,多个附件将一并提交,单文件不超过 20MB
|
||||
</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="refundDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="refundSubmitLoading"
|
||||
@click="submitRefundFinance"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
确认提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="paymentDetailVisible"
|
||||
title="缴费信息详情"
|
||||
@@ -503,16 +391,19 @@
|
||||
fetchPaymentDetailList,
|
||||
fetchUpdatePaymentDetailDraft,
|
||||
fetchSubmitReceiptFinance,
|
||||
fetchSubmitRefundFinance,
|
||||
type CollectionProtocolRow,
|
||||
type PaymentDetailDraftPayload
|
||||
} from '@/api/collection-protocol'
|
||||
import {
|
||||
fetchPaymentTransferList,
|
||||
fetchUpdatePaymentTransfer,
|
||||
fetchSubmitPaymentTransferFinance
|
||||
} from '@/api/protocol-sample-payment'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { useAttachmentDropZone } from '@/composables/useAttachmentDropZone'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import {
|
||||
isPaymentDueWithinWarnWindow,
|
||||
protocolStatusLabel,
|
||||
protocolStatusDisplayLabel,
|
||||
protocolStatusTagType,
|
||||
payModeLabel,
|
||||
@@ -527,7 +418,7 @@
|
||||
|
||||
defineOptions({ name: 'CellStoragePaymentInfoTab' })
|
||||
|
||||
type PayStatusKey = 'dfj' | 'jfqrz' | 'yjf' | 'tfqrz' | 'ytf'
|
||||
type PayStatusKey = 'dfj' | 'jfqrz' | 'yjf'
|
||||
|
||||
interface KanbanColumnDef {
|
||||
key: PayStatusKey
|
||||
@@ -542,12 +433,11 @@
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
/** P0:看板仅保留收款三列;退费列下线(旧 detail 退费态仍可由财务旧接口处理) */
|
||||
const kanbanColumns: KanbanColumnDef[] = [
|
||||
{ key: 'dfj', label: '待缴费' },
|
||||
{ key: 'jfqrz', label: '缴费确认中' },
|
||||
{ key: 'yjf', label: '已缴费' },
|
||||
{ key: 'tfqrz', label: '退费确认中' },
|
||||
{ key: 'ytf', label: '已退费' }
|
||||
{ key: 'yjf', label: '已缴费' }
|
||||
]
|
||||
|
||||
function createColumnState(): KanbanColumnState {
|
||||
@@ -559,9 +449,7 @@
|
||||
const columnStates = reactive<Record<PayStatusKey, KanbanColumnState>>({
|
||||
dfj: createColumnState(),
|
||||
jfqrz: createColumnState(),
|
||||
yjf: createColumnState(),
|
||||
tfqrz: createColumnState(),
|
||||
ytf: createColumnState()
|
||||
yjf: createColumnState()
|
||||
})
|
||||
const expandedCardIds = ref<Set<string>>(new Set())
|
||||
const viewVisible = ref(false)
|
||||
@@ -580,6 +468,7 @@
|
||||
const receiptSaveLoading = ref(false)
|
||||
const receiptSubmitLoading = ref(false)
|
||||
const receiptTargetId = ref('')
|
||||
const receiptIsTransfer = ref(false)
|
||||
const receiptPreviewRow = ref<Record<string, unknown> | null>(null)
|
||||
const receiptFormRef = ref<FormInstance>()
|
||||
const receiptForm = reactive({
|
||||
@@ -594,24 +483,6 @@
|
||||
transferAmount: [{ required: true, message: '请填写转账金额', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const refundDialogVisible = ref(false)
|
||||
const refundSubmitLoading = ref(false)
|
||||
const refundTargetIds = ref<string[]>([])
|
||||
const refundFormRef = ref<FormInstance>()
|
||||
const refundForm = reactive({
|
||||
refundAmount: null as number | null,
|
||||
refundRemark: '',
|
||||
attachments: [] as Array<{
|
||||
id: string
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
_uploading?: boolean
|
||||
}>
|
||||
})
|
||||
const refundFormRules: FormRules = {
|
||||
refundAmount: [{ required: true, message: '请填写退费金额', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
let loadRequestId = 0
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
@@ -625,18 +496,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isTransferRow(row: Record<string, unknown>) {
|
||||
return row.source === 'formal' || Boolean(row.sampleCode && row.scheduleId)
|
||||
}
|
||||
|
||||
async function loadColumn(key: PayStatusKey) {
|
||||
const col = columnStates[key]
|
||||
col.loading = true
|
||||
try {
|
||||
const res = await fetchPaymentDetailList({
|
||||
const q = listQueryParams()
|
||||
const [legacy, transfer] = await Promise.all([
|
||||
fetchPaymentDetailList({
|
||||
page: col.page,
|
||||
pageSize: col.pageSize,
|
||||
payStatus: key,
|
||||
...listQueryParams()
|
||||
...q
|
||||
}),
|
||||
fetchPaymentTransferList({
|
||||
page: col.page,
|
||||
pageSize: col.pageSize,
|
||||
payStatus: key,
|
||||
keyword: q.keyword,
|
||||
installmentDueDateStart: q.installmentDueDateStart,
|
||||
installmentDueDateEnd: q.installmentDueDateEnd
|
||||
})
|
||||
col.list = res.list || []
|
||||
col.total = res.total ?? 0
|
||||
])
|
||||
let transferList = transfer.list || []
|
||||
if (q.transferType) {
|
||||
transferList = transferList.filter(
|
||||
(r) => String(r.transferType || '') === String(q.transferType)
|
||||
)
|
||||
}
|
||||
// 新转账笔优先展示,其后拼接旧 detail(version=1)
|
||||
col.list = [...transferList, ...(legacy.list || [])]
|
||||
col.total = (transfer.total ?? 0) + (legacy.total ?? 0)
|
||||
} finally {
|
||||
col.loading = false
|
||||
}
|
||||
@@ -742,12 +635,11 @@
|
||||
const id = String(row.id || '')
|
||||
if (!id) return
|
||||
receiptTargetId.value = id
|
||||
receiptIsTransfer.value = isTransferRow(row)
|
||||
receiptPreviewRow.value = row
|
||||
receiptForm.transferType = String(row.transferType || '')
|
||||
receiptForm.transferAmount =
|
||||
row.transferAmount != null && row.transferAmount !== ''
|
||||
? Number(row.transferAmount)
|
||||
: null
|
||||
row.transferAmount != null && row.transferAmount !== '' ? Number(row.transferAmount) : null
|
||||
receiptForm.transferTime = row.transferTime ? String(row.transferTime) : ''
|
||||
receiptForm.remark = String(row.remark || '')
|
||||
receiptForm.attachments = mapRowAttachments(row.attachments, row.transferAttachments)
|
||||
@@ -843,7 +735,12 @@
|
||||
}
|
||||
receiptSaveLoading.value = true
|
||||
try {
|
||||
await fetchUpdatePaymentDetailDraft(receiptTargetId.value, buildReceiptPayload())
|
||||
const payload = buildReceiptPayload()
|
||||
if (receiptIsTransfer.value) {
|
||||
await fetchUpdatePaymentTransfer(receiptTargetId.value, payload)
|
||||
} else {
|
||||
await fetchUpdatePaymentDetailDraft(receiptTargetId.value, payload)
|
||||
}
|
||||
receiptDialogVisible.value = false
|
||||
loadAllColumns()
|
||||
} finally {
|
||||
@@ -874,7 +771,13 @@
|
||||
}
|
||||
receiptSubmitLoading.value = true
|
||||
try {
|
||||
await fetchSubmitReceiptFinance(receiptTargetId.value, buildReceiptPayload())
|
||||
const payload = buildReceiptPayload()
|
||||
if (receiptIsTransfer.value) {
|
||||
await fetchUpdatePaymentTransfer(receiptTargetId.value, payload)
|
||||
await fetchSubmitPaymentTransferFinance(receiptTargetId.value)
|
||||
} else {
|
||||
await fetchSubmitReceiptFinance(receiptTargetId.value, payload)
|
||||
}
|
||||
receiptDialogVisible.value = false
|
||||
loadAllColumns()
|
||||
} finally {
|
||||
@@ -882,31 +785,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function resetRefundForm(defaultAmount?: number) {
|
||||
refundForm.refundAmount = defaultAmount ?? null
|
||||
refundForm.refundRemark = ''
|
||||
refundForm.attachments = []
|
||||
refundFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function openRefundFinanceDialog(row: Record<string, unknown>) {
|
||||
const id = String(row.id || '')
|
||||
if (!id) return
|
||||
refundTargetIds.value = [id]
|
||||
const defaultAmount = row.transferAmount != null ? Number(row.transferAmount) : undefined
|
||||
resetRefundForm(
|
||||
defaultAmount != null && !Number.isNaN(defaultAmount) ? defaultAmount : undefined
|
||||
)
|
||||
refundDialogVisible.value = true
|
||||
}
|
||||
|
||||
function refundAttachmentsToUrls() {
|
||||
return refundForm.attachments
|
||||
.filter((f) => f.fileUrl && !f._uploading)
|
||||
.map((f) => f.fileUrl)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
function fullFileUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
@@ -917,70 +795,6 @@
|
||||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url)
|
||||
}
|
||||
|
||||
async function handleRefundAttachmentChange(uploadFile: { raw?: File }) {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
const tempId = `temp_${Date.now()}`
|
||||
refundForm.attachments.push({
|
||||
id: tempId,
|
||||
fileName: file.name,
|
||||
fileUrl: '',
|
||||
_uploading: true
|
||||
})
|
||||
const idx = refundForm.attachments.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', 'protocol-payment-refund')
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
refundForm.attachments[idx] = {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
_uploading: false
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
refundForm.attachments.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function removeRefundAttachment(index: number) {
|
||||
refundForm.attachments.splice(index, 1)
|
||||
}
|
||||
|
||||
const { isDragover: isRefundAttachDragover, dropZoneHandlers: refundAttachDropHandlers } =
|
||||
useAttachmentDropZone({
|
||||
onFiles: (files) => {
|
||||
for (const file of files) {
|
||||
void handleRefundAttachmentChange({ raw: file })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function submitRefundFinance() {
|
||||
if (!refundTargetIds.value.length) return
|
||||
const valid = await refundFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (refundForm.attachments.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
refundAmount: Number(refundForm.refundAmount),
|
||||
refundRemark: refundForm.refundRemark?.trim() || undefined,
|
||||
refundAttachments: refundAttachmentsToUrls() || undefined
|
||||
}
|
||||
refundSubmitLoading.value = true
|
||||
try {
|
||||
await fetchSubmitRefundFinance(refundTargetIds.value[0], payload)
|
||||
refundDialogVisible.value = false
|
||||
loadAllColumns()
|
||||
} finally {
|
||||
refundSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadAllColumns(true)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,12 @@
|
||||
><el-tag
|
||||
:type="protocolStatusTagType(String(protocol?.protocolStatus || ''))"
|
||||
size="small"
|
||||
>{{ protocolStatusDisplayLabel(String(protocol?.protocolStatus || ''), String(protocol?.voidType || '')) }}</el-tag
|
||||
>{{
|
||||
protocolStatusDisplayLabel(
|
||||
String(protocol?.protocolStatus || ''),
|
||||
String(protocol?.voidType || '')
|
||||
)
|
||||
}}</el-tag
|
||||
></div
|
||||
></div
|
||||
>
|
||||
@@ -91,7 +96,9 @@
|
||||
<template v-if="protocol?.voidTime">
|
||||
<div class="data-item"
|
||||
><div class="data-label">作废类型</div
|
||||
><div class="data-value">{{ voidTypeLabel(String(protocol?.voidType || '')) || '—' }}</div></div
|
||||
><div class="data-value">{{
|
||||
voidTypeLabel(String(protocol?.voidType || '')) || '—'
|
||||
}}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">协议作废时间</div
|
||||
@@ -229,7 +236,9 @@
|
||||
<template v-if="protocol?.voidTime">
|
||||
<div class="data-item"
|
||||
><div class="data-label">作废类型</div
|
||||
><div class="data-value">{{ voidTypeLabel(String(protocol?.voidType || '')) || '—' }}</div></div
|
||||
><div class="data-value">{{
|
||||
voidTypeLabel(String(protocol?.voidType || '')) || '—'
|
||||
}}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">协议作废时间</div
|
||||
@@ -641,11 +650,115 @@
|
||||
<!-- 缴费信息 -->
|
||||
<div id="view-section-payment" class="section-card">
|
||||
<div class="section-title section-title--banner"><h3>缴费信息</h3></div>
|
||||
|
||||
<!-- v2: 按样本缴费只读 -->
|
||||
<template v-if="paymentModelVersion >= 2">
|
||||
<el-alert
|
||||
v-if="presignDepositHint"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 12px"
|
||||
:title="presignDepositHintText"
|
||||
/>
|
||||
<div class="payment-summary">
|
||||
<div class="payment-metric">
|
||||
<div class="pm-label">协议总金额(元)</div>
|
||||
<div class="pm-value pm-value--amount">{{
|
||||
fmtMoney(protocol?.protocolTotalAmount)
|
||||
}}</div>
|
||||
</div>
|
||||
<div class="payment-metric">
|
||||
<div class="pm-label">样本合同合计(元)</div>
|
||||
<div class="pm-value pm-value--amount">{{ fmtMoney(sampleContractSum) }}</div>
|
||||
</div>
|
||||
<div class="payment-metric">
|
||||
<div class="pm-label">已确认收款(元)</div>
|
||||
<div class="pm-value pm-value--amount pm-value--received">{{
|
||||
fmtMoney(v2ReceivedAmount)
|
||||
}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-subtitle">按样本缴费方案</div>
|
||||
<div v-if="samplePayments.length" class="sample-pay-readonly">
|
||||
<div
|
||||
v-for="(sp, sIdx) in samplePayments"
|
||||
:key="sp.id || sIdx"
|
||||
class="sample-pay-readonly__card"
|
||||
>
|
||||
<div class="sample-pay-readonly__head">
|
||||
<span
|
||||
>{{ sp.sampleCode || '未编码' }} ·
|
||||
{{ payModeLabel(String(sp.payMode || '')) }}</span
|
||||
>
|
||||
<span class="sample-pay-readonly__amt">{{ fmtMoney(sp.contractAmount) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(sch, schIdx) in sp.schedules || []"
|
||||
:key="sch.id || schIdx"
|
||||
class="sample-pay-readonly__sch"
|
||||
>
|
||||
<div class="sample-pay-readonly__sch-hd">
|
||||
第 {{ sch.scheduleNo || schIdx + 1 }} 期 · 应缴
|
||||
{{ fmtMoney(sch.dueAmount) }}
|
||||
<template v-if="sch.dueDate"> · {{ formatYmd(sch.dueDate) }}</template>
|
||||
· 已缴 {{ fmtMoney(sch.paidAmount) }} · 欠缴
|
||||
{{ fmtMoney(sch.arrearsAmount) }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(tr, tIdx) in sch.transfers || []"
|
||||
:key="tr.id || tIdx"
|
||||
class="sample-pay-readonly__tr"
|
||||
>
|
||||
<el-tag :type="payStatusTagType(String(tr.payStatus || ''))" size="small">
|
||||
{{ payStatusLabel(String(tr.payStatus || '')) }}
|
||||
</el-tag>
|
||||
<span>{{ transferTypeLabel(String(tr.transferType || '')) || '—' }}</span>
|
||||
<span>{{ fmtMoney(tr.transferAmount) }}</span>
|
||||
<span>{{ formatDisplayOrDash(tr.transferTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无按样本缴费方案" :image-size="40" />
|
||||
<div class="section-subtitle">退费登记</div>
|
||||
<el-table
|
||||
v-if="refundRecords.length"
|
||||
:data="refundRecords"
|
||||
class="list-table inner-table"
|
||||
stripe
|
||||
>
|
||||
<el-table-column
|
||||
prop="refundAmount"
|
||||
label="退费金额"
|
||||
min-width="120"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ fmtMoney(row.refundAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="refundTime" label="退费时间" min-width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDisplayOrDash(row.refundTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="refundRemark"
|
||||
label="备注"
|
||||
min-width="180"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
<el-empty v-else description="暂无退费登记" :image-size="40" />
|
||||
</template>
|
||||
|
||||
<!-- v1: 旧整单缴费只读 -->
|
||||
<template v-else>
|
||||
<!-- 缴费主表指标卡 -->
|
||||
<div class="payment-summary">
|
||||
<div class="payment-metric"
|
||||
><div class="pm-label">缴费模式</div
|
||||
><div class="pm-value">{{ payModeLabel(String(payment?.payMode || '')) }}</div></div
|
||||
><div class="pm-value">{{
|
||||
payModeLabel(String(payment?.payMode || ''))
|
||||
}}</div></div
|
||||
>
|
||||
<div class="payment-metric"
|
||||
><div class="pm-label">合同金额(元)</div
|
||||
@@ -743,11 +856,17 @@
|
||||
<span class="info-label">转账时间</span>
|
||||
<span class="info-value">{{ dash(detail.transferTime) }}</span>
|
||||
</div>
|
||||
<div v-if="isInstallmentPayMode(String(payment?.payMode || ''))" class="pdc-info-item">
|
||||
<div
|
||||
v-if="isInstallmentPayMode(String(payment?.payMode || ''))"
|
||||
class="pdc-info-item"
|
||||
>
|
||||
<span class="info-label">应缴费时间</span>
|
||||
<span class="info-value">{{ formatYmd(detail.installmentDueDate) }}</span>
|
||||
</div>
|
||||
<div v-if="isInstallmentPayMode(String(payment?.payMode || ''))" class="pdc-info-item">
|
||||
<div
|
||||
v-if="isInstallmentPayMode(String(payment?.payMode || ''))"
|
||||
class="pdc-info-item"
|
||||
>
|
||||
<span class="info-label">分期期序号</span>
|
||||
<span class="info-value">{{ dash(detail.installmentNo) }}</span>
|
||||
</div>
|
||||
@@ -765,8 +884,8 @@
|
||||
|
||||
<!-- Mini flow: 正向横向进度条 + 下方详情卡片 -->
|
||||
<template
|
||||
v-for="flowSteps in [buildMiniFlowSteps(detail)]"
|
||||
:key="`${detail.id || dIdx}-flow`"
|
||||
v-for="(flowSteps, flowIdx) in [buildMiniFlowSteps(detail)]"
|
||||
:key="`${detail.id || dIdx}-flow-${flowIdx}`"
|
||||
>
|
||||
<div v-if="flowSteps.length" class="flow-section">
|
||||
<div class="mini-flow">
|
||||
@@ -846,7 +965,9 @@
|
||||
class="flow-detail-card__kv"
|
||||
>
|
||||
<span class="flow-detail-card__label">确认时间</span>
|
||||
<span class="flow-detail-card__value">{{ formatDisplayOrDash(detail.updateTime) }}</span>
|
||||
<span class="flow-detail-card__value">{{
|
||||
formatDisplayOrDash(detail.updateTime)
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
@@ -915,7 +1036,9 @@
|
||||
</div>
|
||||
<div v-if="detail.updateTime" class="flow-detail-card__kv">
|
||||
<span class="flow-detail-card__label">确认时间</span>
|
||||
<span class="flow-detail-card__value">{{ formatDisplayOrDash(detail.updateTime) }}</span>
|
||||
<span class="flow-detail-card__value">{{
|
||||
formatDisplayOrDash(detail.updateTime)
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
@@ -953,8 +1076,8 @@
|
||||
|
||||
<!-- 缴费总览 Timeline -->
|
||||
<template
|
||||
v-for="overviewSteps in [buildOverviewTimelineSteps()]"
|
||||
:key="'overview-timeline'"
|
||||
v-for="(overviewSteps, overviewIdx) in [buildOverviewTimelineSteps()]"
|
||||
:key="`overview-timeline-${overviewIdx}`"
|
||||
>
|
||||
<div v-if="overviewSteps.length > 1" class="payment-timeline">
|
||||
<div class="timeline-title">缴费总览</div>
|
||||
@@ -985,6 +1108,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 审核记录 -->
|
||||
@@ -1014,7 +1138,8 @@
|
||||
label="审核时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center">
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -1127,14 +1252,13 @@
|
||||
sampleStatusTagType,
|
||||
transportTypeLabel,
|
||||
payStatusLabel,
|
||||
payStatusTagType,
|
||||
acceptConclusionLabel,
|
||||
acceptConclusionTagType
|
||||
payStatusTagType
|
||||
} from '@/utils/status-tag-registry'
|
||||
import { loadTransferTypeDict, transferTypeLabel } from '@/utils/protocol-payment-labels'
|
||||
import { loadVoidTypeDict, voidTypeLabel } from '@/utils/void-type-dict'
|
||||
import { formatProtocolDocSubject } from '@/utils/protocol-doc-label'
|
||||
import { isInstallmentPayMode } from '@/utils/protocol-payment-presets'
|
||||
import { sumSampleContractAmounts } from '@/utils/protocol-sample-payment'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -1197,6 +1321,39 @@
|
||||
const partyCustomer = computed(() => bundle.value?.partyCustomer as Record<string, any> | null)
|
||||
const collectionInfo = computed(() => bundle.value?.collectionInfo as Record<string, any> | null)
|
||||
const payment = computed(() => bundle.value?.payment as Record<string, any> | null)
|
||||
const paymentModelVersion = computed(() =>
|
||||
Number((bundle.value?.protocol as any)?.paymentModelVersion || 1)
|
||||
)
|
||||
const samplePayments = computed(
|
||||
() => (bundle.value?.samplePayments as Array<Record<string, any>>) || []
|
||||
)
|
||||
const refundRecords = computed(
|
||||
() => (bundle.value?.refundRecords as Array<Record<string, any>>) || []
|
||||
)
|
||||
const presignDepositHint = computed(
|
||||
() => (bundle.value?.presignDepositHint as Record<string, any>) || null
|
||||
)
|
||||
const sampleContractSum = computed(() => sumSampleContractAmounts(samplePayments.value))
|
||||
const presignDepositHintText = computed(() => {
|
||||
const h = presignDepositHint.value
|
||||
if (!h) return ''
|
||||
const amt = Number(h.confirmedDepositAmount || 0)
|
||||
const cnt = Number(h.confirmedTransferCount || 0)
|
||||
return `来源预签约已确认意向金合计 ¥${formatMoneyDisplay(amt)}(${cnt} 笔),仅提示不自动计入样本缴费`
|
||||
})
|
||||
const v2ReceivedAmount = computed(() => {
|
||||
let sum = 0
|
||||
for (const sp of samplePayments.value) {
|
||||
for (const sch of sp.schedules || []) {
|
||||
for (const tr of sch.transfers || []) {
|
||||
if (String(tr.payStatus || '') === 'yjf') {
|
||||
sum += Number(tr.transferAmount || 0) || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sum
|
||||
})
|
||||
const samples = computed(() => {
|
||||
const rows = (bundle.value?.samples as Array<Record<string, any>>) || []
|
||||
return [...rows].sort((a, b) => {
|
||||
@@ -1387,12 +1544,6 @@
|
||||
|
||||
function dash(val: unknown) {
|
||||
return formatDisplayOrDash(val)
|
||||
}
|
||||
|
||||
function formatMetric(v: unknown) {
|
||||
if (v === null || v === undefined || v === '') return '—'
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? String(n) : '—'
|
||||
}
|
||||
|
||||
function formatYmd(val: unknown) {
|
||||
@@ -1525,8 +1676,7 @@
|
||||
|
||||
const steps: OverviewTimelineStep[] = details.map((d, i) => {
|
||||
const s = String(d.payStatus || '')
|
||||
const name =
|
||||
isInstallmentPayMode(String(payment.value?.payMode || ''))
|
||||
const name = isInstallmentPayMode(String(payment.value?.payMode || ''))
|
||||
? `第${d.installmentNo || i + 1}期`
|
||||
: `缴费${i + 1}`
|
||||
|
||||
@@ -1609,14 +1759,6 @@
|
||||
return sampleTypeOptions.value.find((x) => x.id === id)?.typeName || dash(id)
|
||||
}
|
||||
|
||||
function unitName(id: string, fallbackName?: string) {
|
||||
const fromOptions = unitOptions.value.find((x) => x.id === id)?.unitName
|
||||
if (fromOptions) return fromOptions
|
||||
const name = String(fallbackName || '').trim()
|
||||
if (name) return name
|
||||
return dash(id)
|
||||
}
|
||||
|
||||
function ensureUnitsInOptions(samples: Array<Record<string, any>>) {
|
||||
const merged = [...unitOptions.value]
|
||||
for (const s of samples) {
|
||||
@@ -1664,11 +1806,6 @@
|
||||
return '—'
|
||||
}
|
||||
|
||||
function sampleReceptionCheckList(reception: Record<string, any>) {
|
||||
const list = reception?.checkResults
|
||||
return Array.isArray(list) ? list : []
|
||||
}
|
||||
|
||||
function sampleReceptionAttachmentFiles(reception: Record<string, any>) {
|
||||
const list = reception?.receiveAttachmentFiles
|
||||
if (Array.isArray(list) && list.length) return list
|
||||
@@ -2540,6 +2677,55 @@
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.sample-pay-readonly {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sample-pay-readonly__card {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sample-pay-readonly__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sample-pay-readonly__amt {
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.sample-pay-readonly__sch {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.sample-pay-readonly__sch-hd {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.sample-pay-readonly__tr {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
/* ---- Mini Flow ---- */
|
||||
.mini-flow {
|
||||
display: flex;
|
||||
|
||||
Reference in new issue
Block a user