""" 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 ] }