fix(action-plans): derive overdue filter in list endpoint (single source with stats)
ci/woodpecker/push/main Pipeline failed

Frontend sends card key status=overdue to GET /api/cma/action-plans, but the

list endpoint compared ActionPlan.status == 'overdue' which is not in the

status domain -> always 0 rows while /stats showed 7 (derived caliber).

Extract _apply_overdue() and use it in both plan_stats and list_plans.

Refs: project-20260921-actionplan-overdue-filter
This commit is contained in:
Hermes CI Fix
2026-09-21 15:13:27 +08:00
parent 27ea7e7f53
commit f88ed94c15
2 changed files with 330 additions and 3 deletions
+33 -3
View File
@@ -62,6 +62,32 @@ def _validate_due_date_against_quarter(due_date: datetime, quarter: str):
f"KR截止日期({due.date()})超出本季度范围({q_end.date()}),最大截止为{q_end.date()}")
# ──────────────────────────────────────────────
# 「已逾期」派生口径(单一来源,2026-09-21 project-20260921-actionplan-overdue-filter)
# ──────────────────────────────────────────────
# 口径:overdue = status in (pending, in_progress) AND due_date < now
#
# 为什么要有这个函数(缺陷根因):status 的取值域只有
# pending / in_progress / completed / cancelled —— **没有** overdue 这个值。
# 前端把统计卡片的 key('overdue')直接当 status 传给列表端点
# (frontend/src/views/ActionPlanLibrary.vue:414 卡片 key、:26 filterStatus、:422 params.status),
# 而 list_plans 原实现按 `ActionPlan.status == 'overdue'` 直查 → 恒 0 条,
# 于是出现「卡片 7 条 ≠ 列表 0 条」。
# 统计端点 plan_stats 早就用的是派生口径 —— 两处口径分家正是本次不一致的来源。
# 故把派生口径抽成本函数,plan_stats 与 list_plans **共用同一实现**(同源),
# 今后口径只此一处,不再出现「卡片 ≠ 列表」。
OVERDUE_KEY = "overdue"
OVERDUE_STATUSES = ("pending", "in_progress")
def _apply_overdue(query):
"""把「已逾期」派生口径施加到 ActionPlan 查询上(stats / list 共用)。"""
return query.filter(
ActionPlan.status.in_(list(OVERDUE_STATUSES)),
ActionPlan.due_date < datetime.now(),
)
def plan_to_dict(p: ActionPlan) -> dict:
return {
"id": p.id,
@@ -100,7 +126,12 @@ def list_plans(
"""获取行动计划列表(账套隔离: join KPI按企业过滤, 2026-08-23 P1b)"""
query = db.query(ActionPlan).join(KPIDefinition, KPIDefinition.id == ActionPlan.kpi_id).filter(KPIDefinition.entity_id == entity_id).order_by(ActionPlan.created_at.desc())
if status:
if status == OVERDUE_KEY:
# 「已逾期」是**派生条件**而非 status 值(口径与 /stats 同源,2026-09-21)
# 前端统计卡片直接把 key='overdue' 当 status 传来,故此处按派生口径过滤,
# 与 plan_stats 共用 _apply_overdue,保证「卡片条数 = 列表条数」。
query = _apply_overdue(query)
elif status:
query = query.filter(ActionPlan.status == status)
if kpi_id:
query = query.filter(ActionPlan.kpi_id == kpi_id)
@@ -352,8 +383,7 @@ def plan_stats(db: Session = Depends(get_db), current_user: User = Depends(requi
pending = query.filter(ActionPlan.status == "pending").count()
in_progress = query.filter(ActionPlan.status == "in_progress").count()
completed = query.filter(ActionPlan.status == "completed").count()
from datetime import datetime
overdue = query.filter(ActionPlan.status.in_(["pending", "in_progress"]), ActionPlan.due_date < datetime.now()).count()
overdue = _apply_overdue(query).count()
return {
"total": total,
"pending": pending,