Files
shanxia2.0/docs/superpowers/specs/2026-08-20-pl-plan-contract-complete-design.md
T
2026-08-27 19:38:01 +08:00

219 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运输计划合同完善 · 设计说明
> Demo:`docs/fresh-mint-pl-plan-contract-complete-demo.html`(方案 B 为采纳交互)
> 日期:2026-08-20
> 状态:已实现(待联调验收)
## 1. 背景与目标
运输计划新增/编辑时,供需双方合同已改为**非必填**,便于先建计划再补合同。业务上:非内置公司一侧仍应关联合同;内置公司该侧合同可免。
**内置公司(合同可免):**
- 安庆市山峡化工有限公司
- 安徽云栋化工有限公司
判定逻辑复用现有工具:
- 前端:`web-admin/src/utils/pl-internal-customer.ts` → `isPlInternalCustomerName`
- 后端:`backend/app/services/pl_internal_customer.py` → `is_internal_customer_name`
**目标:** 提供独立的「运输计划合同完善」界面,只列出仍缺合同的未作废计划,并支持在列表行内直接关联已有 CRM 合同(不支持在本页新增/编辑合同)。
## 2. 已确认决策
| 项 | 结论 |
|---|---|
| 实现路径 | 独立菜单页 + 专用列表/保存 API(路径 1) |
| 交互 | 列表「补充」打开弹窗关联本段合同(已由行内展开改为弹窗,见 `docs/fresh-mint-pl-plan-contract-supplement-dialog-demo.html`) |
| 计划范围 | 仅未作废(`follow_status != cancelled`),含待安排/已安排/运输中等;**已归档但未作废的计划仍入列**(未采用「排除归档」) |
| 合同数量 | 每侧可多选(与计划新增/编辑一致) |
| 保存方式 | 两侧各自「保存」;补完一侧即可落库 |
| 菜单角标 | 要;显示当前缺合同条数(对齐危化品证) |
| 非目标 | 本页不新增/编辑/审批合同;不改计划其它字段;不做小程序页 |
## 3. 信息架构与入口
### 3.1 菜单
挂在 **运输管理 → 运输计划** 分组下,与「运输计划」「运输计划归档」并列:
| 项 | 值 |
|---|---|
| 菜单名 | 运输计划合同完善 |
| Hash 路由 | `/#/pl/plan-module/plan-contract` |
| 组件 | `web-admin/src/views/pl/plan-contract/index.vue` |
| menuCode | `pl:plan-contract:list` |
| 更新权限 | `pl:plan-contract:update` |
路由注册:`web-admin/src/router/modules/pl.ts`(`plan-module` children)。
菜单/权限 SQL:写入 `docs/sql`(与现有 pl 菜单脚本风格一致)。
### 3.2 UI 模式
**P02 复杂业务列表**,参考:
- `web-admin/src/views/pl/plan-cert/index.vue`(待办型衍生列表 + 角标)
- `web-admin/src/views/pl/plan/index.vue`(列表骨架、筛选、日期快捷)
视觉遵循 `docs/web-admin-ui-spec-fresh-mint.md`。
页面骨架:
```
panel
├─ panel-top:标题 + 待完善数量
├─ filter-block
├─ list-toolbar(日期快捷 + 快速搜索 + 刷新;无「新增」)
├─ list-table(可展开行)
└─ pagination
```
## 4. 缺口判定规则
对每条未删除、未作废的运输计划:
1. **供方侧**
- 若 `is_internal_customer_name(supplier_name)` → 该侧**不要求**合同(可免)
- 否则 → 至少 1 条有效供方侧 `pl_plan_contract`(`side=supplier`, `del_flag=0`)
2. **需方侧**
- 对 `customer_name` 同理(`side=demander`)
3. **入列表条件**:供方侧缺口 **或** 需方侧缺口(任一侧不满足要求)
列表/角标口径必须与上述规则一致(前后端同一工具函数)。
## 5. 列表与行内展开
### 5.1 列表列
| 列 | 说明 |
|---|---|
| 计划编号 | 主色链接 → 打开现有计划只读查看 |
| 计划日期 | |
| 供方 | 名称;副文案:已关联 N 份 / 未关联 / 内置可免 |
| 需方 | 同上 |
| 合同缺口 | Tag:`缺供方合同` / `缺需方合同` / `两侧均缺` |
| 跟进状态 | 现有跟进状态 Tag |
| 操作 | 「展开补充」/「收起」;「查看计划」 |
### 5.2 筛选与排序
- 筛选:计划编号、缺口类型(全部 / 仅缺供方 / 仅缺需方 / 两侧均缺)、供方或需方关键词、计划日期区间
- 工具栏:日期快捷 + 快速搜索(计划编号/供方/需方)
- 默认排序:`plan_date DESC`,其次 `create_time DESC`
- 数据权限:与运输计划列表相同(`apply_pl_scope`)
### 5.3 行内展开交互
- 同时仅展开一行;展开另一行时收起当前行
- 展开区左右两栏:供方合同 | 需方合同
| 侧状态 | UI |
|---|---|
| 待补充 | 已选合同卡片(可移除)+「选择合同」+ 该侧「保存」 |
| 已齐 | 展示已关联合同;仍允许改选后保存 |
| 可免(内置) | 灰态说明;无选择器、无保存按钮 |
- 「选择合同」复用 `ContractSelector`(多选;可选状态与计划页一致:已审批 / 执行中);按该侧 `customerId` 过滤
- **不提供**新建/编辑合同入口
- 保存只提交**该侧**合同列表;成功后刷新该行;若两侧均已满足缺口规则,该行从列表移除并刷新角标
- 有未保存变更时收起:二次确认
组件复用:优先复用 `PlPlanContractMultiSelect` / `ContractSelector` 的选择与卡片展示能力。
## 6. API 设计
建议挂在运输计划模块下(路径示例,实现时可微调前缀,须注册到 `app/api` + `main`):
### 6.1 缺合同分页列表
`GET /api/pl/plans/contract-gaps`
Query:分页 + `planNo`、`gapType`(`supplier` | `demander` | `both`)、客户关键词、`planDate` 起止、快速搜索等。
固定过滤:`del_flag=0`、`follow_status != cancelled`、存在合同缺口(**不**按 `archive_status` 排除)。
响应行至少包含:
- 计划基础字段(编号、日期、供需方 id/name、跟进状态等)
- `gapSides: ("supplier" | "demander")[]`
- `supplierContractExempt` / `demanderContractExempt`
- `supplierContracts` / `demanderContracts`(`PlanContractLink[]`,展开回填)
### 6.2 单侧保存合同
`PUT /api/pl/plans/{planId}/contracts/{side}`
- `side` ∈ `supplier` | `demander`
- Body:`{ "contracts": PlanContractLink[] }`(可多份;空数组表示清空该侧)
服务端:
1. 校验计划存在、未作废、租户与数据权限
2. 校验合同可选(归属该侧客户、状态允许等),复用 `assert_contract_selectable_for_plan` / `validate_plan_party_contracts` 中与合同相关的能力;**本接口不因「非内置且空列表」直接 422**(允许先清空;列表是否仍展示由缺口规则决定)
3. 加载对侧现有合同 + 本侧请求列表 → 调用现有 `replace_plan_contracts` 整体替换关联表
4. 返回:更新后两侧合同 + 最新 `gapSides`(及豁免标记)
**不走**完整 `PlanUpdate`;不修改产品、运输、客户等字段。
### 6.3 与现有「合同非必填」的关系
计划创建/全量更新已不再强制非内置侧必须有合同。本页是事后补齐通道;主计划编辑页仍可改合同,行为保持一致。
## 7. 菜单角标
- `menuCode`:`pl:plan-contract:list`
- 在 `backend/app/services/pl_menu_badge_service.py` 增加计数函数,口径与列表默认条件一致
- 加入 `SUPPORTED_MENU_CODES`;前端侧栏展示数字(对齐 `pl:plan-cert:list`)
## 8. 权限
| 权限码 | 用途 |
|---|---|
| `pl:plan-contract:list` | 进入页面、拉列表、角标 |
| `pl:plan-contract:update` | 单侧保存合同 |
「查看计划」复用现有计划查看能力(与 `plan-cert` 跳转查看一致,通常依赖 `pl:plan:list` 或已有详情权限)。
## 9. 前端文件(预期)
| 文件 | 职责 |
|---|---|
| `web-admin/src/views/pl/plan-contract/index.vue` | 页面 |
| `web-admin/src/router/modules/pl.ts` | 路由 |
| `web-admin/src/api/pl/*` | list gaps / save side |
| (可选)小组件 | 行内展开双侧合同区,避免单文件过大 |
## 10. 后端文件(预期)
| 文件 | 职责 |
|---|---|
| `app/api/pl/plan.py` 或独立 router | 两个端点 |
| `app/services/pl_plan_service.py`(或小服务模块) | 缺口查询、单侧保存编排 |
| `app/schemas/pl/plan.py` | 请求/响应模型 |
| `app/services/pl_menu_badge_service.py` | 角标 |
| `docs/sql/*` | 菜单与权限 |
## 11. 错误与边界
- 已作废计划:列表不出现;若 ID 直调保存 → 400/422,提示不可修改
- 合同不属于该侧客户或状态不可选 → 422,明确文案
- 并发:单侧保存以「读对侧最新 + 写本侧」再 `replace_plan_contracts`;后写覆盖该次替换结果(可接受;不做乐观锁,除非后续有冲突投诉)
- 内置侧误传合同:允许关联(不强制拒绝),UI 默认不展示选择器
## 12. 测试要点
- 缺口判定:双非内置无合同 → 入列;一侧内置一侧外部无合同 → 仅缺外部侧;双侧内置 → 不入列
- 单侧保存后对侧合同不变
- 补齐后行消失、角标减一
- 作废计划不出现在列表与角标
- 数据权限用户看不到范围外计划
- 前端:展开互斥、未保存收起确认、无新建合同入口
## 13. 非目标(明确不做)
- 合同 CRUD / 审批
- 修改运输计划主数据、产品、运输安排
- 小程序「合同完善」页
- 主计划列表增加「缺合同」Tab(本设计采用独立页)