fix(action-plans): derive overdue filter in list endpoint (single source with stats)
ci/woodpecker/push/main Pipeline failed
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user