161 lines
5.7 KiB
Python
161 lines
5.7 KiB
Python
"""
|
||
OKR目标管理 API — 季度目标 + 关键结果 + KPI联动
|
||
"""
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from sqlalchemy.orm import Session
|
||
from sqlalchemy import func
|
||
from datetime import datetime, timedelta
|
||
from typing import Optional
|
||
from app.database import get_db
|
||
from app.auth_middleware import require_role
|
||
from app.models import Objective, ActionPlan
|
||
|
||
router = APIRouter(prefix="/api/cma/okr", tags=["OKR目标管理"],
|
||
dependencies=[Depends(require_role("ceo", "finance", "it"))],
|
||
)
|
||
|
||
|
||
@router.get("")
|
||
def list_objectives(
|
||
quarter: Optional[str] = Query(None, description="筛选季度: 2026Q3"),
|
||
dimension: Optional[str] = Query(None),
|
||
status: Optional[str] = Query(None),
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""列出OKR目标"""
|
||
q = db.query(Objective)
|
||
if quarter:
|
||
q = q.filter(Objective.quarter == quarter)
|
||
if dimension:
|
||
q = q.filter(Objective.dimension == dimension)
|
||
if status:
|
||
q = q.filter(Objective.status == status)
|
||
objs = q.order_by(Objective.quarter.desc(), Objective.id).all()
|
||
results = []
|
||
for o in objs:
|
||
krs = db.query(ActionPlan).filter(ActionPlan.objective_id == o.id).all()
|
||
kr_summary = [
|
||
{"id": kr.id, "title": kr.title, "status": kr.status, "progress": kr.progress}
|
||
for kr in krs
|
||
]
|
||
results.append({
|
||
"id": o.id, "title": o.title, "description": o.description,
|
||
"dimension": o.dimension, "quarter": o.quarter,
|
||
"owner": o.owner, "status": o.status, "progress": o.progress,
|
||
"confidence": o.confidence,
|
||
"key_results": kr_summary,
|
||
"kr_count": len(krs),
|
||
"kr_completed": sum(1 for kr in krs if kr.status == "completed"),
|
||
"created_at": o.created_at.isoformat() if o.created_at else None,
|
||
})
|
||
return {"total": len(results), "items": results}
|
||
|
||
|
||
@router.post("")
|
||
def create_objective(
|
||
data: dict,
|
||
db: Session = Depends(get_db),
|
||
):
|
||
"""创建OKR目标(支持JSON Body和Query参数两种方式)"""
|
||
# 兼容旧版Query参数
|
||
title = data.get("title") or ""
|
||
quarter = data.get("quarter") or ""
|
||
description = data.get("description")
|
||
dimension = data.get("dimension")
|
||
owner = data.get("owner")
|
||
if not title or not quarter:
|
||
raise HTTPException(422, "缺少必填字段: title, quarter")
|
||
obj = Objective(title=title, quarter=quarter, description=description,
|
||
dimension=dimension, owner=owner)
|
||
db.add(obj)
|
||
db.commit()
|
||
db.refresh(obj)
|
||
return {"ok": True, "id": obj.id, "title": obj.title}
|
||
|
||
|
||
@router.get("/{obj_id}")
|
||
def get_objective(obj_id: int, db: Session = Depends(get_db)):
|
||
"""获取单个OKR详情"""
|
||
obj = db.query(Objective).filter(Objective.id == obj_id).first()
|
||
if not obj:
|
||
raise HTTPException(404, "目标不存在")
|
||
krs = db.query(ActionPlan).filter(ActionPlan.objective_id == obj_id).all()
|
||
return {
|
||
"objective": {
|
||
"id": obj.id, "title": obj.title, "description": obj.description,
|
||
"dimension": obj.dimension, "quarter": obj.quarter,
|
||
"owner": obj.owner, "status": obj.status, "progress": obj.progress,
|
||
"confidence": obj.confidence,
|
||
},
|
||
"key_results": [
|
||
{"id": kr.id, "title": kr.title, "kpi_id": kr.kpi_id,
|
||
"status": kr.status, "progress": kr.progress,
|
||
"due_date": kr.due_date.isoformat() if kr.due_date else None,
|
||
"assignee": kr.assignee}
|
||
for kr in krs
|
||
]
|
||
}
|
||
|
||
|
||
@router.patch("/{obj_id}")
|
||
def update_objective(obj_id: int, db: Session = Depends(get_db)):
|
||
"""更新OKR进度(通过查询ActionPlan自动计算)"""
|
||
obj = db.query(Objective).filter(Objective.id == obj_id).first()
|
||
if not obj:
|
||
raise HTTPException(404, "目标不存在")
|
||
krs = db.query(ActionPlan).filter(ActionPlan.objective_id == obj_id).all()
|
||
if krs:
|
||
obj.progress = sum(kr.progress for kr in krs) // len(krs)
|
||
db.commit()
|
||
return {"ok": True, "id": obj_id, "progress": obj.progress}
|
||
|
||
|
||
@router.get("/{okr_id}/decomposition")
|
||
def get_okr_decomposition(okr_id: int, db: Session = Depends(get_db)):
|
||
"""获取OKR的时间分解视图数据"""
|
||
okr = db.query(Objective).filter(Objective.id == okr_id).first()
|
||
if not okr:
|
||
raise HTTPException(404, "OKR不存在")
|
||
|
||
# 1. 关联的BSC战略O(年度 — 相同维度且没有季度标识)
|
||
bsc_o = db.query(Objective).filter(
|
||
Objective.dimension == okr.dimension,
|
||
Objective.quarter.is_(None)
|
||
).first()
|
||
|
||
# 2. 本OKR的所有KR(关联到该Objective的ActionPlan)
|
||
krs = db.query(ActionPlan).filter(ActionPlan.objective_id == okr_id).all()
|
||
|
||
# 3. 当前周的ActionPlan(本周行动计划)
|
||
now = datetime.now()
|
||
week_start = now - timedelta(days=now.weekday())
|
||
week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
week_end = week_start + timedelta(days=7)
|
||
action_plans = db.query(ActionPlan).filter(
|
||
ActionPlan.objective_id == okr_id,
|
||
ActionPlan.due_date.between(week_start, week_end)
|
||
).all()
|
||
|
||
return {
|
||
"annual_o": bsc_o.title if bsc_o else None,
|
||
"quarterly_o": okr.title,
|
||
"krs": [
|
||
{
|
||
"kr_id": kr.id,
|
||
"title": kr.title,
|
||
"progress": kr.progress,
|
||
"milestones": kr.monthly_milestones or []
|
||
}
|
||
for kr in krs
|
||
],
|
||
"weekly_actions": [
|
||
{
|
||
"id": ap.id,
|
||
"title": ap.title,
|
||
"status": ap.status,
|
||
"deadline": ap.due_date.isoformat() if ap.due_date else None
|
||
}
|
||
for ap in action_plans
|
||
]
|
||
}
|