|
|
@@ -0,0 +1,735 @@
|
|
|
+# 运输计划列表「送货时间」列 Implementation Plan
|
|
|
+
|
|
|
+> **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:** 在运输计划族四页列表「计划日期」后展示去重升序的送货段日期(`deliveryTimes`),并支持列头区间过滤(任一送货段命中)。
|
|
|
+
|
|
|
+**Architecture:** 在 `build_plan_column_filters` 增加对 `pl_plan_delivery.delivery_time` 的 EXISTS 过滤;列表装配时从已加载送货段聚合 `deliveryTimes: string[]`;四页 Vue 增加合并列 + 列头漏斗;计划维度导出在「计划日期」后增加「送货时间」(` / ` 拼接)。不使用、不改写 `pl_plan.delivery_time`。
|
|
|
+
|
|
|
+**Tech Stack:** FastAPI · SQLAlchemy · Pydantic v2 · Vue 3 · Element Plus · pytest
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-09-12-pl-plan-delivery-times-column-design.md`
|
|
|
+
|
|
|
+**提交约定:** 默认每个 Task 末尾 commit;若用户要求暂不提交,可攒到全部完成后再提交。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件地图
|
|
|
+
|
|
|
+| 职责 | 路径 |
|
|
|
+|------|------|
|
|
|
+| 过滤 EXISTS | Modify `backend/app/services/pl/plan_list_filters.py` |
|
|
|
+| 过滤单测 | Modify `backend/tests/test_pl_plan_list_filters.py` |
|
|
|
+| 聚合 helper + attach | Modify `backend/app/services/pl_plan_stop_service.py` |
|
|
|
+| 聚合单测 | Create `backend/tests/test_pl_delivery_times_aggregate.py` |
|
|
|
+| Schema | Modify `backend/app/schemas/pl/plan.py`(`PlanWithProducts`、`PlanContractGapRead`) |
|
|
|
+| List/Export/Gaps API | Modify `backend/app/api/pl/plan.py` |
|
|
|
+| Gaps service 过滤+字段 | Modify `backend/app/services/pl_plan_contract_gap.py` |
|
|
|
+| FE Plan 类型 | Modify `web-admin/src/api/pl/index.ts` |
|
|
|
+| FE 列定义 | Modify 四页 `columnFilterDefs.ts` |
|
|
|
+| FE 四页列表 | Modify `plan` / `plan-archive` / `plan-cert` / `plan-contract` 的 `index.vue` |
|
|
|
+
|
|
|
+**不做:** DB 迁移、小程序、归档段次展开导出加聚合列、改顶部日期 pills。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 过滤 EXISTS + 单测
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/services/pl/plan_list_filters.py`
|
|
|
+- Modify: `backend/tests/test_pl_plan_list_filters.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败单测**
|
|
|
+
|
|
|
+在 `backend/tests/test_pl_plan_list_filters.py` 末尾追加:
|
|
|
+
|
|
|
+```python
|
|
|
+def test_delivery_time_range_exists_requires_db():
|
|
|
+ filters = build_plan_column_filters(
|
|
|
+ db=None,
|
|
|
+ tenant_no="shanxia2.0",
|
|
|
+ delivery_time_from=date(2026, 9, 12),
|
|
|
+ delivery_time_to=date(2026, 9, 14),
|
|
|
+ )
|
|
|
+ assert filters == []
|
|
|
+
|
|
|
+
|
|
|
+def test_delivery_time_range_exists_sql(monkeypatch):
|
|
|
+ class _FakeQuery:
|
|
|
+ def __init__(self):
|
|
|
+ self.filters = []
|
|
|
+
|
|
|
+ def filter(self, *args):
|
|
|
+ self.filters.extend(args)
|
|
|
+ return self
|
|
|
+
|
|
|
+ def exists(self):
|
|
|
+ return ("EXISTS", tuple(self.filters))
|
|
|
+
|
|
|
+ class _FakeDb:
|
|
|
+ def query(self, *_args, **_kwargs):
|
|
|
+ return _FakeQuery()
|
|
|
+
|
|
|
+ filters = build_plan_column_filters(
|
|
|
+ db=_FakeDb(),
|
|
|
+ tenant_no="shanxia2.0",
|
|
|
+ delivery_time_from=date(2026, 9, 12),
|
|
|
+ delivery_time_to="2026-09-14",
|
|
|
+ )
|
|
|
+ assert len(filters) == 1
|
|
|
+ kind, clauses = filters[0]
|
|
|
+ assert kind == "EXISTS"
|
|
|
+ compiled = " ".join(_compile(c) for c in clauses)
|
|
|
+ assert "pl_plan_delivery" in compiled or "delivery_time" in compiled
|
|
|
+ assert ">=" in compiled
|
|
|
+ assert "<=" in compiled
|
|
|
+```
|
|
|
+
|
|
|
+说明:若实现选用 SQLAlchemy `exists()` 表达式而非假 query 元组,将 `test_delivery_time_range_exists_sql` 改为对真实 `exists` clause 做 `_compile`,断言含 `exists`、`delivery_time`、`>=`、`<=`、`del_flag`(与现有 product_name exists 测法一致即可)。**以能编译出 EXISTS 子查询且不引用 `pl_plan.delivery_time` 为准。**
|
|
|
+
|
|
|
+- [ ] **Step 2: 跑测确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_filters.py::test_delivery_time_range_exists_requires_db tests/test_pl_plan_list_filters.py::test_delivery_time_range_exists_sql -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(未知参数或断言失败)
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现过滤**
|
|
|
+
|
|
|
+在 `plan_list_filters.py`:
|
|
|
+
|
|
|
+1. Import `PlPlanDelivery`:
|
|
|
+```python
|
|
|
+from app.models.pl_models import PlPlan, PlPlanProduct, PlPlanDelivery
|
|
|
+```
|
|
|
+
|
|
|
+2. 增加 helper(推荐真实 exists,便于编译断言):
|
|
|
+
|
|
|
+```python
|
|
|
+def plan_delivery_time_exists(
|
|
|
+ db: Session,
|
|
|
+ tenant_no: str,
|
|
|
+ *,
|
|
|
+ d_from: Optional[date] = None,
|
|
|
+ d_to: Optional[date] = None,
|
|
|
+):
|
|
|
+ """任一未删除送货段的送货日历日落在 [from, to] 内。"""
|
|
|
+ q = db.query(PlPlanDelivery.id).filter(
|
|
|
+ PlPlanDelivery.plan_id == PlPlan.id,
|
|
|
+ PlPlanDelivery.tenant_no == tenant_no,
|
|
|
+ PlPlanDelivery.del_flag == 0,
|
|
|
+ PlPlanDelivery.delivery_time.isnot(None),
|
|
|
+ )
|
|
|
+ if d_from is not None:
|
|
|
+ q = q.filter(func.date(PlPlanDelivery.delivery_time) >= d_from)
|
|
|
+ if d_to is not None:
|
|
|
+ q = q.filter(func.date(PlPlanDelivery.delivery_time) <= d_to)
|
|
|
+ return q.exists()
|
|
|
+```
|
|
|
+
|
|
|
+3. `build_plan_column_filters` 增加参数并在 `plan_date` 过滤后追加:
|
|
|
+
|
|
|
+```python
|
|
|
+ delivery_time_from: DateLike = None,
|
|
|
+ delivery_time_to: DateLike = None,
|
|
|
+```
|
|
|
+
|
|
|
+```python
|
|
|
+ dt_from = _parse_date(delivery_time_from)
|
|
|
+ dt_to = _parse_date(delivery_time_to)
|
|
|
+ if (dt_from is not None or dt_to is not None) and db is not None:
|
|
|
+ filters.append(
|
|
|
+ plan_delivery_time_exists(
|
|
|
+ db, tenant_no, d_from=dt_from, d_to=dt_to
|
|
|
+ )
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+若采用真实 `exists()`,把 Step 1 的 fake-db 测试改成:
|
|
|
+
|
|
|
+```python
|
|
|
+def test_delivery_time_range_exists_sql():
|
|
|
+ from unittest.mock import MagicMock
|
|
|
+
|
|
|
+ db = MagicMock()
|
|
|
+ q = MagicMock()
|
|
|
+ db.query.return_value = q
|
|
|
+ q.filter.return_value = q
|
|
|
+ q.exists.return_value = "DELIVERY_TIME_EXISTS"
|
|
|
+
|
|
|
+ filters = build_plan_column_filters(
|
|
|
+ db=db,
|
|
|
+ tenant_no="shanxia2.0",
|
|
|
+ delivery_time_from=date(2026, 9, 12),
|
|
|
+ delivery_time_to="2026-09-14",
|
|
|
+ )
|
|
|
+ assert filters == ["DELIVERY_TIME_EXISTS"]
|
|
|
+ assert db.query.called
|
|
|
+ # filter 调用中应包含 date 比较;至少确认 exists 被调用
|
|
|
+ assert q.exists.called
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测通过**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_filters.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/pl/plan_list_filters.py backend/tests/test_pl_plan_list_filters.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(pl): filter plans by delivery-stop date range
|
|
|
+
|
|
|
+Add EXISTS on pl_plan_delivery.delivery_time for list column filters.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: `deliveryTimes` 聚合 helper + 单测
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/services/pl_plan_stop_service.py`
|
|
|
+- Create: `backend/tests/test_pl_delivery_times_aggregate.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败单测**
|
|
|
+
|
|
|
+创建 `backend/tests/test_pl_delivery_times_aggregate.py`:
|
|
|
+
|
|
|
+```python
|
|
|
+from datetime import date, datetime
|
|
|
+
|
|
|
+from app.services.pl_plan_stop_service import collect_unique_delivery_date_strings
|
|
|
+
|
|
|
+
|
|
|
+class _D:
|
|
|
+ def __init__(self, delivery_time):
|
|
|
+ self.delivery_time = delivery_time
|
|
|
+
|
|
|
+
|
|
|
+def test_collect_unique_sorted_yyyy_mm_dd():
|
|
|
+ rows = [
|
|
|
+ _D(datetime(2026, 9, 14, 10, 0, 0)),
|
|
|
+ _D(datetime(2026, 9, 12, 8, 0, 0)),
|
|
|
+ _D(date(2026, 9, 12)),
|
|
|
+ _D(None),
|
|
|
+ _D(""),
|
|
|
+ _D("2026-09-13"),
|
|
|
+ ]
|
|
|
+ assert collect_unique_delivery_date_strings(rows) == [
|
|
|
+ "2026-09-12",
|
|
|
+ "2026-09-13",
|
|
|
+ "2026-09-14",
|
|
|
+ ]
|
|
|
+
|
|
|
+
|
|
|
+def test_collect_empty():
|
|
|
+ assert collect_unique_delivery_date_strings([]) == []
|
|
|
+ assert collect_unique_delivery_date_strings([_D(None)]) == []
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 跑测确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_pl_delivery_times_aggregate.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(ImportError / 函数不存在)
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现 helper**
|
|
|
+
|
|
|
+在 `pl_plan_stop_service.py` 合适位置(`attach_plan_stops_to_item` 附近)增加:
|
|
|
+
|
|
|
+```python
|
|
|
+def collect_unique_delivery_date_strings(deliveries) -> list[str]:
|
|
|
+ """送货段 delivery_time → 去重升序 YYYY-MM-DD。"""
|
|
|
+ days: set[date] = set()
|
|
|
+ for row in deliveries or []:
|
|
|
+ raw = getattr(row, "delivery_time", None)
|
|
|
+ if raw is None or raw == "":
|
|
|
+ continue
|
|
|
+ if isinstance(raw, datetime):
|
|
|
+ days.add(raw.date())
|
|
|
+ elif isinstance(raw, date):
|
|
|
+ days.add(raw)
|
|
|
+ else:
|
|
|
+ text = str(raw).strip()
|
|
|
+ if not text:
|
|
|
+ continue
|
|
|
+ try:
|
|
|
+ days.add(datetime.strptime(text[:10], "%Y-%m-%d").date())
|
|
|
+ except ValueError:
|
|
|
+ continue
|
|
|
+ return [d.isoformat() for d in sorted(days)]
|
|
|
+```
|
|
|
+
|
|
|
+确保文件顶部已有 `date` / `datetime` import(已有则勿重复)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 跑测通过**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_pl_delivery_times_aggregate.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/pl_plan_stop_service.py backend/tests/test_pl_delivery_times_aggregate.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(pl): aggregate unique delivery dates from stop rows
|
|
|
+
|
|
|
+Provide YYYY-MM-DD dedupe helper for plan list deliveryTimes.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: Schema + attach 写入 `deliveryTimes`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/schemas/pl/plan.py`
|
|
|
+- Modify: `backend/app/services/pl_plan_stop_service.py`(`attach_plan_stops_to_item`)
|
|
|
+- Modify: `backend/app/services/pl_plan_contract_gap.py`(`_build_gap_item` 或等价构造函数)
|
|
|
+
|
|
|
+- [ ] **Step 1: Schema 增加字段**
|
|
|
+
|
|
|
+在 `PlanWithProducts`(约 `delivery_customer_summary` 后)增加:
|
|
|
+
|
|
|
+```python
|
|
|
+ delivery_times: list[str] = Field(
|
|
|
+ default_factory=list,
|
|
|
+ alias="deliveryTimes",
|
|
|
+ description="送货段送货时间去重升序 YYYY-MM-DD(列表展示)",
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+在 `PlanContractGapRead` 增加同样字段(`deliveries` 字段附近):
|
|
|
+
|
|
|
+```python
|
|
|
+ delivery_times: list[str] = Field(
|
|
|
+ default_factory=list,
|
|
|
+ alias="deliveryTimes",
|
|
|
+ description="送货段送货时间去重升序 YYYY-MM-DD",
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: attach 时写入**
|
|
|
+
|
|
|
+`attach_plan_stops_to_item` 末尾:
|
|
|
+
|
|
|
+```python
|
|
|
+ plan_item.delivery_times = collect_unique_delivery_date_strings(delivery_reads)
|
|
|
+```
|
|
|
+
|
|
|
+(若 Pydantic model 用 alias,赋值用 `delivery_times` 属性名。)
|
|
|
+
|
|
|
+- [ ] **Step 3: 合同缺口项写入**
|
|
|
+
|
|
|
+在构造 `PlanContractGapRead(...)` 处增加:
|
|
|
+
|
|
|
+```python
|
|
|
+ deliveryTimes=collect_unique_delivery_date_strings(deliveries),
|
|
|
+```
|
|
|
+
|
|
|
+或 `delivery_times=...`(与该处其它字段传参风格一致)。从 `pl_plan_stop_service` import `collect_unique_delivery_date_strings`。
|
|
|
+
|
|
|
+- [ ] **Step 4: 快速冒烟**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && python -c "
|
|
|
+from app.schemas.pl.plan import PlanWithProducts, PlanContractGapRead
|
|
|
+from app.services.pl_plan_stop_service import collect_unique_delivery_date_strings, attach_plan_stops_to_item
|
|
|
+print('ok', collect_unique_delivery_date_strings([]))
|
|
|
+"
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 打印 `ok []`,无 ImportError
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/schemas/pl/plan.py backend/app/services/pl_plan_stop_service.py backend/app/services/pl_plan_contract_gap.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(pl): expose deliveryTimes on plan list and contract-gap schemas
|
|
|
+
|
|
|
+Populate from delivery stops during attach/gap row build.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: API 透传过滤参数 + 计划导出列
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/api/pl/plan.py`
|
|
|
+- Modify: `backend/app/services/pl_plan_contract_gap.py`(`list_plan_contract_gaps` 签名与过滤)
|
|
|
+
|
|
|
+- [ ] **Step 1: `_filters_from_params` 透传**
|
|
|
+
|
|
|
+```python
|
|
|
+ delivery_time_from=_param_get(params, "deliveryTimeFrom", "delivery_time_from"),
|
|
|
+ delivery_time_to=_param_get(params, "deliveryTimeTo", "delivery_time_to"),
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: `list_plans` Query + `_merge_query`**
|
|
|
+
|
|
|
+在 `plan_date_to` Query 旁增加:
|
|
|
+
|
|
|
+```python
|
|
|
+ delivery_time_from: Optional[date] = Query(
|
|
|
+ default=None, alias="deliveryTimeFrom", description="送货时间开始"
|
|
|
+ ),
|
|
|
+ delivery_time_to: Optional[date] = Query(
|
|
|
+ default=None, alias="deliveryTimeTo", description="送货时间结束"
|
|
|
+ ),
|
|
|
+```
|
|
|
+
|
|
|
+并在 `_merge_query(...)` 调用处传入 `deliveryTimeFrom=delivery_time_from`、`deliveryTimeTo=delivery_time_to`。
|
|
|
+
|
|
|
+证书列表若走同一 `list_plans`,无需另改。
|
|
|
+
|
|
|
+- [ ] **Step 3: Export body + 计划维度导出填充**
|
|
|
+
|
|
|
+`PlanExportBody` 增加(可依赖 `extra="allow"`,但显式字段更清晰):
|
|
|
+
|
|
|
+```python
|
|
|
+ delivery_time_from: Optional[date] = Field(default=None, alias="deliveryTimeFrom")
|
|
|
+ delivery_time_to: Optional[date] = Field(default=None, alias="deliveryTimeTo")
|
|
|
+```
|
|
|
+
|
|
|
+`PLAN_EXPORT_COLUMNS` 在计划日期后插入:
|
|
|
+
|
|
|
+```python
|
|
|
+ ExportColumn(title="计划日期", field="planDate"),
|
|
|
+ ExportColumn(title="送货时间", field="deliveryTimes"),
|
|
|
+```
|
|
|
+
|
|
|
+非 archive 导出循环中,在 `model_validate(plan)` 后批量加载 stops(页内已有 `load_plan_stops_grouped` 用法):
|
|
|
+
|
|
|
+```python
|
|
|
+ plan_ids = [p.id for p in plans]
|
|
|
+ stops_by_plan = load_plan_stops_grouped(db, tenant_no, plan_ids)
|
|
|
+ items = []
|
|
|
+ for plan in plans:
|
|
|
+ data = PlanWithProducts.model_validate(plan).model_dump(by_alias=True)
|
|
|
+ if data.get("planDate") is not None:
|
|
|
+ data["planDate"] = str(data["planDate"])[:10]
|
|
|
+ _, delivery_reads = stops_by_plan.get(plan.id, (None, []))
|
|
|
+ times = collect_unique_delivery_date_strings(delivery_reads)
|
|
|
+ data["deliveryTimes"] = " / ".join(times) if times else ""
|
|
|
+ if data.get("erpEntered") is not None:
|
|
|
+ data["erpEntered"] = "是" if int(data["erpEntered"]) else "否"
|
|
|
+ items.append(data)
|
|
|
+```
|
|
|
+
|
|
|
+从 `pl_plan_stop_service` import `collect_unique_delivery_date_strings`(若尚未导入)。**不要**改 `PLAN_ARCHIVE_EXPORT_COLUMNS`(段次展开已有「段次时间」)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 合同缺口 list/export 透传**
|
|
|
+
|
|
|
+`list_contract_gaps` / `export_contract_gaps`:Query/Body + 调用 `list_plan_contract_gaps(..., delivery_time_from=..., delivery_time_to=...)`。
|
|
|
+
|
|
|
+在 `list_plan_contract_gaps` 内,于 `plan_date` 过滤附近增加:
|
|
|
+
|
|
|
+```python
|
|
|
+ from app.services.pl.plan_list_filters import plan_delivery_time_exists, _parse_date
|
|
|
+
|
|
|
+ dt_from = _parse_date(delivery_time_from)
|
|
|
+ dt_to = _parse_date(delivery_time_to)
|
|
|
+ if dt_from is not None or dt_to is not None:
|
|
|
+ query = query.filter(
|
|
|
+ plan_delivery_time_exists(db, tenant_no, d_from=dt_from, d_to=dt_to)
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+(若 `_parse_date` 不想导出,可在 gap 服务内复用 `build_plan_column_filters` 只取 delivery 条件,或把 `_parse_date` 升为模块公开函数;优先直接调用 `build_plan_column_filters` 把返回的 filters `query.filter(*filters)`,但注意不要重复已有 plan_no 等条件——**仅追加 delivery 相关 EXISTS**:调用 `plan_delivery_time_exists` 最干净。)
|
|
|
+
|
|
|
+`CONTRACT_GAP_EXPORT_COLUMNS` 在计划日期后加「送货时间」;export 行把 `deliveryTimes` list 转 `" / ".join(...)`。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/api/pl/plan.py backend/app/services/pl_plan_contract_gap.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(pl): wire deliveryTimeFrom/To through plan list and export
|
|
|
+
|
|
|
+Contract-gap list/export share the same stop-date EXISTS filter.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 前端类型 + 四页 `columnFilterDefs`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/api/pl/index.ts`
|
|
|
+- Modify: `web-admin/src/views/pl/plan/columnFilterDefs.ts`
|
|
|
+- Modify: `web-admin/src/views/pl/plan-archive/columnFilterDefs.ts`
|
|
|
+- Modify: `web-admin/src/views/pl/plan-cert/columnFilterDefs.ts`
|
|
|
+- Modify: `web-admin/src/views/pl/plan-contract/columnFilterDefs.ts`
|
|
|
+
|
|
|
+- [ ] **Step 1: Plan / Gap 类型**
|
|
|
+
|
|
|
+在 `Plan` interface(`planDate` 附近)增加:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ deliveryTimes?: string[]
|
|
|
+```
|
|
|
+
|
|
|
+若合同缺口有独立 interface(搜 `PlanContractGap` / gap 列表类型),同样加 `deliveryTimes?: string[]`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 四页 columnFilterDefs**
|
|
|
+
|
|
|
+在各文件的 `planDate` 定义后插入:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ {
|
|
|
+ prop: 'deliveryTimes',
|
|
|
+ label: '送货时间',
|
|
|
+ type: 'datetime',
|
|
|
+ queryKeyFrom: 'deliveryTimeFrom',
|
|
|
+ queryKeyTo: 'deliveryTimeTo'
|
|
|
+ },
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/api/pl/index.ts \
|
|
|
+ web-admin/src/views/pl/plan/columnFilterDefs.ts \
|
|
|
+ web-admin/src/views/pl/plan-archive/columnFilterDefs.ts \
|
|
|
+ web-admin/src/views/pl/plan-cert/columnFilterDefs.ts \
|
|
|
+ web-admin/src/views/pl/plan-contract/columnFilterDefs.ts
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): add deliveryTimes column filter defs for plan lists
|
|
|
+
|
|
|
+Align query keys with deliveryTimeFrom/To server filters.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: 主列表 `plan/index.vue` UI
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/pl/plan/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 合并列与列设置**
|
|
|
+
|
|
|
+`PLAN_MERGE_COLUMN_PROPS` 在 `'planDate'` 后插入 `'deliveryTimes'`。
|
|
|
+
|
|
|
+列设置数组 `{ prop: 'planDate', label: '计划日期' }` 后插入:
|
|
|
+
|
|
|
+```typescript
|
|
|
+ { prop: 'deliveryTimes', label: '送货时间' },
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 表格列(计划日期列后)**
|
|
|
+
|
|
|
+```vue
|
|
|
+ <el-table-column
|
|
|
+ v-if="isColumnVisible('deliveryTimes')"
|
|
|
+ prop="deliveryTimes"
|
|
|
+ label="送货时间"
|
|
|
+ min-width="130"
|
|
|
+ align="center"
|
|
|
+ >
|
|
|
+ <template #header>
|
|
|
+ <div class="col-header-with-filter">
|
|
|
+ <span class="col-header-with-filter__label">送货时间</span>
|
|
|
+ <ColumnHeaderFilter
|
|
|
+ prop="deliveryTimes"
|
|
|
+ label="送货时间"
|
|
|
+ type="datetime"
|
|
|
+ :is-active="isFilterActive('deliveryTimes')"
|
|
|
+ :model-value="filters.deliveryTimes || null"
|
|
|
+ @update:model-value="(v) => setFilterState('deliveryTimes', v)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div
|
|
|
+ v-if="(row.plan.deliveryTimes || []).length"
|
|
|
+ class="pl-delivery-times-cell"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-for="(t, i) in row.plan.deliveryTimes"
|
|
|
+ :key="`${row.plan.id}-dt-${i}`"
|
|
|
+ >
|
|
|
+ {{ t }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <span v-else class="text-muted">—</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+```
|
|
|
+
|
|
|
+若项目无 `.text-muted`,用现有 muted 类或内联 `color: var(--mint-text-muted)`。
|
|
|
+
|
|
|
+- [ ] **Step 3: `getDisplayStatCellValue`**
|
|
|
+
|
|
|
+```typescript
|
|
|
+ if (prop === 'deliveryTimes') {
|
|
|
+ return (plan.deliveryTimes || []).join('\n') || '—'
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+(合并键用字符串化结果,保证同计划多行一致。)
|
|
|
+
|
|
|
+- [ ] **Step 4: 样式**
|
|
|
+
|
|
|
+scoped 中:
|
|
|
+
|
|
|
+```scss
|
|
|
+ .pl-delivery-times-cell {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 2px;
|
|
|
+ line-height: 1.35;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 确认 pills 只清 planDate**
|
|
|
+
|
|
|
+核对 `handleDateShortcut` 仅 `delete filters.planDate`,**不要**删除 `filters.deliveryTimes`。
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/pl/plan/index.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): show deliveryTimes column on transport plan list
|
|
|
+
|
|
|
+Multi-line YYYY-MM-DD with header range filter after planDate.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: 归档 / 证书列表 UI(与主列表同构)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/pl/plan-archive/index.vue`
|
|
|
+- Modify: `web-admin/src/views/pl/plan-cert/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 两页均做与 Task 6 相同改动**
|
|
|
+
|
|
|
+- `PLAN_MERGE_COLUMN_PROPS` 插入 `deliveryTimes`
|
|
|
+- 列设置插入
|
|
|
+- `el-table-column`(数据源仍为 `row.plan.deliveryTimes`)
|
|
|
+- `getDisplayStatCellValue`(或等价)处理 `deliveryTimes`
|
|
|
+- 同样 cell 样式类
|
|
|
+
|
|
|
+- [ ] **Step 2: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/pl/plan-archive/index.vue web-admin/src/views/pl/plan-cert/index.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): add deliveryTimes column on plan archive and cert lists
|
|
|
+
|
|
|
+Match main plan list display and column-header filter behavior.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 8: 合同完善列表 UI + 导出字段
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/pl/plan-contract/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 合并与列**
|
|
|
+
|
|
|
+`PLAN_MERGE_COLUMN_PROPS`:`['planDate', 'deliveryTimes', 'planType', 'followStatus']`(或在 `planDate` 后插入)。
|
|
|
+
|
|
|
+表格在计划日期后增加列;单元格取 `asGapItem(row).deliveryTimes`(与该页 `planDate` 取值方式一致)。
|
|
|
+
|
|
|
+列头过滤 defs 已在 Task 5;确保 `useServerColumnFilters` 使用的 defs 含 `deliveryTimes`,且 buildParams 会带上 `deliveryTimeFrom/To`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 查看弹窗(可选一致性)**
|
|
|
+
|
|
|
+若补充信息 KV 有「计划日期」,在其后加「送货时间」多行或 ` / ` 拼接;无则跳过(YAGNI)。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/pl/plan-contract/index.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): add deliveryTimes column on plan contract-gap list
|
|
|
+
|
|
|
+Filter and display deduped delivery dates beside planDate.
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 9: 回归验证 + 图谱更新
|
|
|
+
|
|
|
+- [ ] **Step 1: 后端测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_filters.py tests/test_pl_delivery_times_aggregate.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 2: 手测清单(对照 spec §4)**
|
|
|
+
|
|
|
+1. `/#/pl/plan`:计划日期后有送货时间;多段同日去重;多日多行 `YYYY-MM-DD`
|
|
|
+2. 列头送货时间区间:任一命中;顶部 pills 仍只动计划日期,不清送货时间过滤
|
|
|
+3. 归档 / 证书 / 合同完善行为一致
|
|
|
+4. 计划列表导出有「送货时间」,多值 ` / ` 分隔
|
|
|
+5. 无送货时间显示 `—`,送货时间过滤不误命中
|
|
|
+
|
|
|
+- [ ] **Step 3: 更新知识图谱**
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh backend
|
|
|
+./scripts/update-graph.sh frontend
|
|
|
+```
|
|
|
+
|
|
|
+或按仓库约定:`graphify update .`(若脚本封装不同,以 `AGENTS.md` 为准)。
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit 图谱(若有 diff)**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/graphify-out web-admin/graphify-out graphify-out 2>/dev/null || true
|
|
|
+git status -sb
|
|
|
+# 仅当有图谱文件变更时:
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+chore: update graphify after deliveryTimes list column
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Self-Review(对照 spec)
|
|
|
+
|
|
|
+| Spec 项 | Task |
|
|
|
+|---------|------|
|
|
|
+| EXISTS 任一命中过滤 | Task 1、4 |
|
|
|
+| `deliveryTimes[]` 去重升序 YYYY-MM-DD | Task 2、3 |
|
|
|
+| 不用 `pl_plan.delivery_time` | Task 1/2 数据源仅 delivery 表/段 |
|
|
|
+| 四页 UI + 合并列 + 列头漏斗 | Task 5–8 |
|
|
|
+| pills 只筛计划日期 | Task 6 Step 5 |
|
|
|
+| 计划维度导出 + 段次导出不加聚合列 | Task 4 |
|
|
|
+| 合同缺口同源 | Task 3、4、8 |
|
|
|
+| 无 DB 迁移 | 全计划无 DDL |
|
|
|
+
|
|
|
+无 TBD 占位;命名统一 `deliveryTimes` / `deliveryTimeFrom` / `deliveryTimeTo` / `collect_unique_delivery_date_strings`。
|