fix: 行动方案库统计卡片—补全API+overdue计算
This commit is contained in:
+115
-74
@@ -1,12 +1,14 @@
|
||||
"""改善行动计划 API — 管理会计OS"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
import re
|
||||
import logging
|
||||
from calendar import monthrange
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_role, require_auth
|
||||
from app.models import ActionPlan, KPIAlert, KPIDefinition, User
|
||||
import logging
|
||||
from app.models import ActionPlan, KPIAlert, KPIDefinition, User, Objective
|
||||
|
||||
logger = logging.getLogger("cma.action_plans")
|
||||
|
||||
@@ -15,11 +17,52 @@ router = APIRouter(prefix="/api/cma/action-plans", tags=["改善行动"],
|
||||
)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 工具函数
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
def _quarter_to_date_range(quarter: str) -> tuple:
|
||||
"""解析季度字符串 '2026Q3' → (start_date, end_date)"""
|
||||
m = re.match(r"^(\d{4})[Qq]([1-4])$", quarter.strip())
|
||||
if not m:
|
||||
return None, None
|
||||
year = int(m.group(1))
|
||||
q = int(m.group(2))
|
||||
month_map = {1: (1, 1), 2: (4, 1), 3: (7, 1), 4: (10, 1)}
|
||||
start_month, start_day = month_map[q]
|
||||
end_month = start_month + 2
|
||||
if end_month > 12:
|
||||
end_month -= 12
|
||||
end_year = year + 1
|
||||
else:
|
||||
end_year = year
|
||||
_, last_day = monthrange(end_year, end_month)
|
||||
return (
|
||||
datetime(year, start_month, start_day, tzinfo=timezone.utc),
|
||||
datetime(end_year, end_month, last_day, 23, 59, 59, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
def _validate_due_date_against_quarter(due_date: datetime, quarter: str):
|
||||
"""校验截止日期是否在季度范围内,不匹配则抛422"""
|
||||
q_start, q_end = _quarter_to_date_range(quarter)
|
||||
if q_start is None:
|
||||
return # 无法解析季度,跳过校验
|
||||
due = due_date if due_date.tzinfo else due_date.replace(tzinfo=timezone.utc)
|
||||
if due < q_start:
|
||||
raise HTTPException(422,
|
||||
f"KR截止日期({due.date()})早于本季度开始({q_start.date()}),请检查")
|
||||
if due > q_end:
|
||||
raise HTTPException(422,
|
||||
f"KR截止日期({due.date()})超出本季度范围({q_end.date()}),最大截止为{q_end.date()}")
|
||||
|
||||
|
||||
def plan_to_dict(p: ActionPlan) -> dict:
|
||||
return {
|
||||
"id": p.id,
|
||||
"alert_id": p.alert_id,
|
||||
"kpi_id": p.kpi_id,
|
||||
"objective_id": p.objective_id,
|
||||
"title": p.title,
|
||||
"description": p.description,
|
||||
"assignee": p.assignee,
|
||||
@@ -34,6 +77,10 @@ def plan_to_dict(p: ActionPlan) -> dict:
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# API 端点
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
@router.get("")
|
||||
def list_plans(
|
||||
status: Optional[str] = None,
|
||||
@@ -44,21 +91,21 @@ def list_plans(
|
||||
):
|
||||
"""获取行动计划列表"""
|
||||
query = db.query(ActionPlan).order_by(ActionPlan.created_at.desc())
|
||||
|
||||
|
||||
if status:
|
||||
query = query.filter(ActionPlan.status == status)
|
||||
if kpi_id:
|
||||
query = query.filter(ActionPlan.kpi_id == kpi_id)
|
||||
if alert_id:
|
||||
query = query.filter(ActionPlan.alert_id == alert_id)
|
||||
|
||||
|
||||
# business角色只看自己的
|
||||
if current_user.role == "business":
|
||||
query = query.filter(
|
||||
(ActionPlan.assignee == current_user.username) |
|
||||
(ActionPlan.assignee == current_user.name)
|
||||
)
|
||||
|
||||
|
||||
plans = query.all()
|
||||
result = []
|
||||
for p in plans:
|
||||
@@ -67,7 +114,7 @@ def list_plans(
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == p.kpi_id).first()
|
||||
item["kpi_name"] = kpi.kpi_name if kpi else "未知KPI"
|
||||
result.append(item)
|
||||
|
||||
|
||||
return {"data": result}
|
||||
|
||||
|
||||
@@ -77,20 +124,30 @@ def create_plan(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_auth),
|
||||
):
|
||||
"""创建改善行动计划"""
|
||||
"""创建改善行动计划(也是OKR的KR)"""
|
||||
required = ["title", "kpi_id"]
|
||||
for field in required:
|
||||
if field not in data:
|
||||
raise HTTPException(400, f"缺少必填字段: {field}")
|
||||
|
||||
|
||||
due_date = datetime.fromisoformat(data["due_date"]) if data.get("due_date") else None
|
||||
|
||||
# 校验截止日期与关联Objective的季度匹配
|
||||
objective_id = data.get("objective_id")
|
||||
if objective_id and due_date:
|
||||
obj = db.query(Objective).filter(Objective.id == objective_id).first()
|
||||
if obj and obj.quarter:
|
||||
_validate_due_date_against_quarter(due_date, obj.quarter)
|
||||
|
||||
plan = ActionPlan(
|
||||
alert_id=data.get("alert_id"),
|
||||
kpi_id=data["kpi_id"],
|
||||
objective_id=objective_id,
|
||||
title=data["title"],
|
||||
description=data.get("description"),
|
||||
assignee=data.get("assignee"),
|
||||
priority=data.get("priority", "medium"),
|
||||
due_date=datetime.fromisoformat(data["due_date"]) if data.get("due_date") else None,
|
||||
due_date=due_date,
|
||||
status="pending",
|
||||
progress=0,
|
||||
created_by=current_user.name or current_user.username,
|
||||
@@ -111,7 +168,7 @@ def update_plan(
|
||||
plan = db.query(ActionPlan).filter(ActionPlan.id == plan_id).first()
|
||||
if not plan:
|
||||
raise HTTPException(404, "计划不存在")
|
||||
|
||||
|
||||
if "title" in data:
|
||||
plan.title = data["title"]
|
||||
if "description" in data:
|
||||
@@ -128,7 +185,7 @@ def update_plan(
|
||||
plan.progress = max(0, min(100, data["progress"]))
|
||||
if "result" in data:
|
||||
plan.result = data["result"]
|
||||
|
||||
|
||||
db.commit()
|
||||
db.refresh(plan)
|
||||
return plan_to_dict(plan)
|
||||
@@ -145,8 +202,32 @@ def delete_plan(plan_id: int, db: Session = Depends(get_db)):
|
||||
return {"message": "已删除"}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
def plan_stats(db: Session = Depends(get_db), current_user: User = Depends(require_auth)):
|
||||
"""行动计划统计"""
|
||||
query = db.query(ActionPlan)
|
||||
if current_user.role == "business":
|
||||
query = query.filter(
|
||||
(ActionPlan.assignee == current_user.username) |
|
||||
(ActionPlan.assignee == current_user.name)
|
||||
)
|
||||
total = query.count()
|
||||
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.deadline < datetime.now()).count()
|
||||
return {
|
||||
"total": total,
|
||||
"pending": pending,
|
||||
"in_progress": in_progress,
|
||||
"completed": completed,
|
||||
"overdue": overdue,
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 功能5: COSO内控自检表 (CMA P1 - COSO五要素)
|
||||
# COSO内控自检表 (CMA P1 - COSO五要素)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
COSO_CHECKLIST_DATA = {
|
||||
@@ -154,16 +235,12 @@ COSO_CHECKLIST_DATA = {
|
||||
"entity_name": "陕西酣客(白酒经销)",
|
||||
"total_score": 46,
|
||||
"max_score": 100,
|
||||
"risk_level": "high", # high / medium / low
|
||||
"risk_level": "high",
|
||||
"risk_label": "高风险",
|
||||
"elements": [
|
||||
{
|
||||
"id": "control_environment",
|
||||
"name": "控制环境",
|
||||
"name_en": "Control Environment",
|
||||
"score": 60,
|
||||
"max_score": 100,
|
||||
"status": "medium",
|
||||
"id": "control_environment", "name": "控制环境", "name_en": "Control Environment",
|
||||
"score": 60, "max_score": 100, "status": "medium",
|
||||
"items": [
|
||||
{"id": "ce_01", "text": "管理层重视内控", "passed": True, "detail": "✅ 任总亲自跟"},
|
||||
{"id": "ce_02", "text": "职责分离", "passed": True, "detail": "✅ 业务≠财务"},
|
||||
@@ -172,24 +249,16 @@ COSO_CHECKLIST_DATA = {
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "risk_assessment",
|
||||
"name": "风险评估",
|
||||
"name_en": "Risk Assessment",
|
||||
"score": 40,
|
||||
"max_score": 100,
|
||||
"status": "low",
|
||||
"id": "risk_assessment", "name": "风险评估", "name_en": "Risk Assessment",
|
||||
"score": 40, "max_score": 100, "status": "low",
|
||||
"items": [
|
||||
{"id": "ra_01", "text": "风险识别机制", "passed": False, "detail": "❌ 没有系统风险清单"},
|
||||
{"id": "ra_02", "text": "风险应对预案", "passed": False, "detail": "❌ 现金断流无预案"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "control_activities",
|
||||
"name": "控制活动",
|
||||
"name_en": "Control Activities",
|
||||
"score": 30,
|
||||
"max_score": 100,
|
||||
"status": "low",
|
||||
"id": "control_activities", "name": "控制活动", "name_en": "Control Activities",
|
||||
"score": 30, "max_score": 100, "status": "low",
|
||||
"items": [
|
||||
{"id": "ca_01", "text": "渠补审批流程", "passed": False, "detail": "❌ 口头谈,无记录"},
|
||||
{"id": "ca_02", "text": "费用审批流程", "passed": False, "detail": "❌ 超预算无拦截"},
|
||||
@@ -197,24 +266,16 @@ COSO_CHECKLIST_DATA = {
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "information_communication",
|
||||
"name": "信息与沟通",
|
||||
"name_en": "Information & Communication",
|
||||
"score": 70,
|
||||
"max_score": 100,
|
||||
"status": "medium",
|
||||
"id": "information_communication", "name": "信息与沟通", "name_en": "Information & Communication",
|
||||
"score": 70, "max_score": 100, "status": "medium",
|
||||
"items": [
|
||||
{"id": "ic_01", "text": "财务报告及时性", "passed": True, "detail": "✅ 月度出表"},
|
||||
{"id": "ic_02", "text": "系统数据互通", "passed": False, "detail": "❌ 进销存≠财务账"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "monitoring",
|
||||
"name": "监控",
|
||||
"name_en": "Monitoring",
|
||||
"score": 30,
|
||||
"max_score": 100,
|
||||
"status": "low",
|
||||
"id": "monitoring", "name": "监控", "name_en": "Monitoring",
|
||||
"score": 30, "max_score": 100, "status": "low",
|
||||
"items": [
|
||||
{"id": "mo_01", "text": "定期内审", "passed": False, "detail": "❌ 无"},
|
||||
{"id": "mo_02", "text": "异常追踪机制", "passed": False, "detail": "❌ 发现异常无跟踪"},
|
||||
@@ -230,12 +291,8 @@ COSO_CHECKLIST_DATA = {
|
||||
"risk_label": "中风险",
|
||||
"elements": [
|
||||
{
|
||||
"id": "control_environment",
|
||||
"name": "控制环境",
|
||||
"name_en": "Control Environment",
|
||||
"score": 70,
|
||||
"max_score": 100,
|
||||
"status": "medium",
|
||||
"id": "control_environment", "name": "控制环境", "name_en": "Control Environment",
|
||||
"score": 70, "max_score": 100, "status": "medium",
|
||||
"items": [
|
||||
{"id": "ce_01", "text": "管理层重视内控", "passed": True, "detail": "✅ 老板直接管"},
|
||||
{"id": "ce_02", "text": "职责分离", "passed": True, "detail": "✅ 业务≠财务≠技术"},
|
||||
@@ -243,48 +300,32 @@ COSO_CHECKLIST_DATA = {
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "risk_assessment",
|
||||
"name": "风险评估",
|
||||
"name_en": "Risk Assessment",
|
||||
"score": 50,
|
||||
"max_score": 100,
|
||||
"status": "low",
|
||||
"id": "risk_assessment", "name": "风险评估", "name_en": "Risk Assessment",
|
||||
"score": 50, "max_score": 100, "status": "low",
|
||||
"items": [
|
||||
{"id": "ra_01", "text": "风险识别机制", "passed": False, "detail": "❌ 无正式风险清单"},
|
||||
{"id": "ra_02", "text": "风险应对预案", "passed": True, "detail": "✅ 重点项目有预案"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "control_activities",
|
||||
"name": "控制活动",
|
||||
"name_en": "Control Activities",
|
||||
"score": 50,
|
||||
"max_score": 100,
|
||||
"status": "low",
|
||||
"id": "control_activities", "name": "控制活动", "name_en": "Control Activities",
|
||||
"score": 50, "max_score": 100, "status": "low",
|
||||
"items": [
|
||||
{"id": "ca_01", "text": "采购审批流程", "passed": True, "detail": "✅ 有标准流程"},
|
||||
{"id": "ca_02", "text": "项目交付流程", "passed": False, "detail": "❌ 验收流程不完善"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "information_communication",
|
||||
"name": "信息与沟通",
|
||||
"name_en": "Information & Communication",
|
||||
"score": 60,
|
||||
"max_score": 100,
|
||||
"status": "medium",
|
||||
"id": "information_communication", "name": "信息与沟通", "name_en": "Information & Communication",
|
||||
"score": 60, "max_score": 100, "status": "medium",
|
||||
"items": [
|
||||
{"id": "ic_01", "text": "财务报告及时性", "passed": True, "detail": "✅ 月度出表"},
|
||||
{"id": "ic_02", "text": "项目沟通机制", "passed": False, "detail": "❌ 跨部门信息滞后"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "monitoring",
|
||||
"name": "监控",
|
||||
"name_en": "Monitoring",
|
||||
"score": 40,
|
||||
"max_score": 100,
|
||||
"status": "low",
|
||||
"id": "monitoring", "name": "监控", "name_en": "Monitoring",
|
||||
"score": 40, "max_score": 100, "status": "low",
|
||||
"items": [
|
||||
{"id": "mo_01", "text": "定期内审", "passed": False, "detail": "❌ 无"},
|
||||
{"id": "mo_02", "text": "异常追踪机制", "passed": True, "detail": "✅ 项目延期有跟踪"},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""预警 API"""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_auth, require_role
|
||||
from app.models import KPIAlert, OperationLog
|
||||
from app.models import KPIAlert, OperationLog, ActionPlan
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("cma.alerts")
|
||||
@@ -24,17 +24,105 @@ def list_alerts(status: str = None, page: int = Query(1, ge=1), db: Session = De
|
||||
@router.post("/{alert_id}/resolve")
|
||||
def resolve_alert(alert_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
alert = db.query(KPIAlert).filter(KPIAlert.id == alert_id).first()
|
||||
if alert:
|
||||
alert.status = "resolved"
|
||||
alert.resolution = data.get("resolution", "")
|
||||
alert.assignee = data.get("assignee", alert.assignee)
|
||||
from datetime import datetime; alert.resolved_at = datetime.now()
|
||||
if not alert:
|
||||
raise HTTPException(404, "预警不存在")
|
||||
alert.status = "resolved"
|
||||
alert.resolution = data.get("resolution", "")
|
||||
alert.assignee = data.get("assignee", alert.assignee)
|
||||
from datetime import datetime; alert.resolved_at = datetime.now()
|
||||
db.commit()
|
||||
db.refresh(alert)
|
||||
return {
|
||||
"message": "已处理",
|
||||
"assignee": alert.assignee,
|
||||
"alert": {c.name: getattr(alert, c.name) for c in KPIAlert.__table__.columns},
|
||||
"suggest_create_action_plan": alert.alert_level == "red",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{alert_id}/process")
|
||||
def process_alert(alert_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
"""标记预警为处理中"""
|
||||
alert = db.query(KPIAlert).filter(KPIAlert.id == alert_id).first()
|
||||
if not alert:
|
||||
raise HTTPException(404, "预警不存在")
|
||||
if alert.status == "resolved":
|
||||
raise HTTPException(400, "已处理的预警不能重复处理")
|
||||
assignee = data.get("assignee")
|
||||
if not assignee:
|
||||
raise HTTPException(400, "缺少处理人")
|
||||
alert.status = "processing"
|
||||
alert.assignee = assignee
|
||||
db.commit()
|
||||
db.refresh(alert)
|
||||
return {"message": "已标记为处理中", "alert": {c.name: getattr(alert, c.name) for c in KPIAlert.__table__.columns}}
|
||||
|
||||
|
||||
@router.post("/{alert_id}/escalate")
|
||||
def escalate_alert(alert_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
"""升级预警级别"""
|
||||
alert = db.query(KPIAlert).filter(KPIAlert.id == alert_id).first()
|
||||
if not alert:
|
||||
raise HTTPException(404, "预警不存在")
|
||||
assignee = data.get("assignee")
|
||||
if not assignee:
|
||||
raise HTTPException(400, "缺少处理人")
|
||||
if alert.status == "resolved":
|
||||
raise HTTPException(400, "已处理的预警不能升级")
|
||||
if alert.alert_level != "red":
|
||||
alert.alert_level = "red"
|
||||
alert.assignee = assignee
|
||||
db.commit()
|
||||
return {"message": "已处理", "assignee": alert.assignee}
|
||||
db.refresh(alert)
|
||||
return {"message": "已升级", "alert": {c.name: getattr(alert, c.name) for c in KPIAlert.__table__.columns}}
|
||||
|
||||
|
||||
@router.get("/check-timeout")
|
||||
def check_alert_timeout(db: Session = Depends(get_db)):
|
||||
"""超时预警检测 — 超过24小时未处理的pending预警自动升级为红色"""
|
||||
from datetime import datetime, timedelta
|
||||
cutoff = datetime.now() - timedelta(hours=24)
|
||||
timeout_alerts = db.query(KPIAlert).filter(
|
||||
KPIAlert.status == "pending",
|
||||
KPIAlert.created_at < cutoff,
|
||||
KPIAlert.alert_level != "red",
|
||||
).all()
|
||||
upgraded_count = 0
|
||||
for alert in timeout_alerts:
|
||||
alert.alert_level = "red"
|
||||
alert.status = "processing"
|
||||
upgraded_count += 1
|
||||
if upgraded_count:
|
||||
db.commit()
|
||||
return {"total_timeout": len(timeout_alerts), "upgraded_count": upgraded_count}
|
||||
|
||||
|
||||
@router.post("/{alert_id}/create-action-plan")
|
||||
def create_action_plan_from_alert(alert_id: int, data: dict, db: Session = Depends(get_db)):
|
||||
"""从预警创建改善行动计划"""
|
||||
alert = db.query(KPIAlert).filter(KPIAlert.id == alert_id).first()
|
||||
if not alert:
|
||||
raise HTTPException(404, "预警不存在")
|
||||
if alert.action_plan_linked_id:
|
||||
raise HTTPException(400, "已关联改善计划")
|
||||
plan = ActionPlan(
|
||||
alert_id=alert.id,
|
||||
kpi_id=alert.kpi_id,
|
||||
title=f"改善: {alert.alert_message}",
|
||||
assignee=data.get("assignee", ""),
|
||||
priority="high" if alert.alert_level == "red" else "medium",
|
||||
created_by=data.get("created_by", ""),
|
||||
)
|
||||
db.add(plan)
|
||||
db.commit()
|
||||
db.refresh(plan)
|
||||
alert.action_plan_linked_id = plan.id
|
||||
db.commit()
|
||||
return {"message": "改善行动计划已创建", "plan_id": plan.id, "priority": plan.priority}
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 功能4: 风险矩阵热力图 (CMA P2 - ERM框架、风险识别四象限)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
RISK_MATRIX_DATA = {
|
||||
"hanke": {
|
||||
|
||||
+61
-7
@@ -4,7 +4,7 @@ OKR目标管理 API — 季度目标 + 关键结果 + KPI联动
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from app.database import get_db
|
||||
from app.auth_middleware import require_role
|
||||
@@ -53,14 +53,18 @@ def list_objectives(
|
||||
|
||||
@router.post("")
|
||||
def create_objective(
|
||||
title: str = Query(...),
|
||||
quarter: str = Query(...),
|
||||
description: Optional[str] = Query(None),
|
||||
dimension: Optional[str] = Query(None),
|
||||
owner: Optional[str] = Query(None),
|
||||
data: dict,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""创建OKR目标"""
|
||||
"""创建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)
|
||||
@@ -104,3 +108,53 @@ def update_objective(obj_id: int, db: Session = Depends(get_db)):
|
||||
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
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user