feat(KR): 行动挂KR完成 — action_plans.kr_id字段+API+前端目标/KR选择+KR中文名自动填+地图krs端点
- action_plans表加kr_id字段(关联krs.id),create/update/list支持kr_id读写
- 创建行动选KR时自动继承objective_id;列表附kr_title
- 前端行动方案库加'所属目标/KR'两级联动选择,选中KR自动带出KPI
- OKR详情页KR卡片下显示达成行动列表(O→KR→行动三层)
- MapCanvasDialogs onKpiChange选中KPI自动填kr.name=kpi.kpi_name(中文)
- 编辑回填英文code KR名转中文显示兜底
- maps.py 新增GET /{map_id}/krs端点(MapReview战略回顾会KR进度)
- 数据修复: krs表9条英文code title→中文名(按kpi_definitions映射)
- 数据迁移: 高置信行动挂KR(费用率→kr10,应收→kr14); 地图32空KR清理
- okr list的kr_summary补metric_kpi_id(前端联动用)
- 新增6个kr_id测试(test_action_plans),全量547通过
This commit is contained in:
@@ -533,6 +533,71 @@ def _parse_kr_date(v):
|
||||
# ── 战略回顾会 聚合接口 ──────────────────────
|
||||
|
||||
|
||||
@router.get("/{map_id}/krs")
|
||||
def get_map_krs(map_id: int, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||
"""战略回顾会 KR进度: 从krs表读取地图各目标的KR (KR修复 2026-08-27)
|
||||
|
||||
地图dimensions JSON中每个带krs的目标节点已同步到objectives+krs表,
|
||||
此处按 节点._objective_id(或 entity+name 回查)取KR。
|
||||
"""
|
||||
from app.models import KPIDefinition, KR, Objective
|
||||
m = db.query(StrategicMap).filter(StrategicMap.id == map_id, StrategicMap.entity_id == entity_id).first()
|
||||
if not m:
|
||||
raise HTTPException(404, "战略地图不存在")
|
||||
|
||||
dims = m.dimensions
|
||||
if isinstance(dims, str):
|
||||
try:
|
||||
dims = json.loads(dims)
|
||||
except Exception:
|
||||
dims = []
|
||||
dims = dims or []
|
||||
|
||||
obj_ids = []
|
||||
for dim in dims:
|
||||
for obj in dim.get("objectives", []):
|
||||
oid = obj.get("_objective_id")
|
||||
if oid:
|
||||
obj_ids.append(int(oid))
|
||||
continue
|
||||
# 兼容无回写字段的旧节点: 按 entity+name 回查
|
||||
name = (obj.get("name") or "").strip()
|
||||
if name:
|
||||
o = db.query(Objective).filter(
|
||||
Objective.entity_id == entity_id,
|
||||
Objective.title == name,
|
||||
).first()
|
||||
if o:
|
||||
obj_ids.append(o.id)
|
||||
|
||||
result = []
|
||||
if obj_ids:
|
||||
krs = db.query(KR).filter(
|
||||
KR.entity_id == entity_id,
|
||||
KR.objective_id.in_(obj_ids),
|
||||
).order_by(KR.objective_id, KR.sort_order, KR.id).all()
|
||||
for kr in krs:
|
||||
mkpi = None
|
||||
if kr.metric_kpi_id:
|
||||
mkpi = db.query(KPIDefinition).filter(KPIDefinition.id == kr.metric_kpi_id).first()
|
||||
result.append({
|
||||
"id": kr.id,
|
||||
"objective_id": kr.objective_id,
|
||||
"title": kr.title,
|
||||
"metric_kpi_id": kr.metric_kpi_id,
|
||||
"metric_kpi_code": mkpi.kpi_code if mkpi else None,
|
||||
"metric_kpi_name": mkpi.kpi_name if mkpi else None,
|
||||
"operator": _parse_kr_operator_target(kr.operator)[0] if hasattr(kr, "operator") and kr.operator else ">=",
|
||||
"weight": float(kr.weight) if kr.weight is not None else None,
|
||||
"target_value": float(kr.target_value) if kr.target_value is not None else None,
|
||||
"current_value": float(kr.current_value) if kr.current_value is not None else None,
|
||||
"progress": kr.progress or 0,
|
||||
"status": kr.status,
|
||||
"due_date": kr.due_date.isoformat() if kr.due_date else None,
|
||||
})
|
||||
return {"data": result}
|
||||
|
||||
|
||||
@router.get("/{map_id}/review")
|
||||
def get_map_review(map_id: int, level: Optional[str] = None, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)):
|
||||
"""战略回顾会:返回目标状态、KPI值、改善行动
|
||||
|
||||
Reference in New Issue
Block a user