"""仪表盘 + 结算路由""" from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import func as sa_func from datetime import date, timedelta from database import get_db from models import ( User, Project, Submission, AIToolCost, ProjectStatus, ProjectType, WorkType, PhaseGroup ) from auth import get_current_user, require_permission from calculations import ( calc_project_settlement, calc_waste_for_project, calc_labor_cost_for_project, calc_ai_tool_cost_for_project, calc_outsource_cost_for_project, calc_overhead_cost_for_project, calc_management_cost_for_project, calc_team_efficiency ) router = APIRouter(prefix="/api", tags=["仪表盘与结算"]) @router.get("/dashboard") def get_dashboard( db: Session = Depends(get_db), current_user: User = Depends(require_permission("dashboard:view")) ): """全局仪表盘数据""" # 项目概览 active = db.query(Project).filter(Project.status == ProjectStatus.IN_PROGRESS).all() completed = db.query(Project).filter(Project.status == ProjectStatus.COMPLETED).all() abandoned = db.query(Project).filter(Project.status == ProjectStatus.ABANDONED).all() # 当月日期范围 today = date.today() month_start = today.replace(day=1) # 本月人力成本 = 提交人成本 + 管理层成本 # 1. 提交人成本(统计本月所有有提交的人的日成本) monthly_submitters = db.query(Submission.user_id, Submission.submit_date).filter( Submission.submit_date >= month_start, Submission.submit_date <= today, ).distinct().all() monthly_labor = 0.0 processed_user_dates = set() for uid, d in monthly_submitters: key = (uid, d) if key not in processed_user_dates: processed_user_dates.add(key) user = db.query(User).filter(User.id == uid).first() if user: monthly_labor += user.daily_cost # 2. 管理层成本(豁免提交角色的人) from models import Role exempt_role_ids = set( r.id for r in db.query(Role).filter(Role.exempt_submission == 1).all() ) if exempt_role_ids: exempt_users = db.query(User).filter( User.is_active == 1, User.monthly_salary > 0, User.role_id.in_(exempt_role_ids), ).all() # 本月有提交记录的工作日数 monthly_working_days = db.query(Submission.submit_date).filter( Submission.submit_date >= month_start, Submission.submit_date <= today, ).distinct().count() # 管理层成本 = 每人日薪 × 本月工作日数 monthly_management = sum(u.daily_cost * monthly_working_days for u in exempt_users) monthly_labor += monthly_management # 本月 AI 工具成本 monthly_ai = db.query(sa_func.sum(AIToolCost.amount)).filter( AIToolCost.record_date >= month_start, AIToolCost.record_date <= today, ).scalar() or 0 # 本月总产出秒数(只算中期动画制作) monthly_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( Submission.submit_date >= month_start, Submission.submit_date <= today, Submission.total_seconds > 0, Submission.project_phase == PhaseGroup.PRODUCTION, ).scalar() or 0 # 活跃人数 active_users = db.query(Submission.user_id).filter( Submission.submit_date >= month_start, ).distinct().count() working_days = max(1, (today - month_start).days + 1) avg_daily = round(monthly_secs / max(1, active_users) / working_days, 1) # 各项目摘要(进度只算中期动画制作) project_summaries = [] for p in active: waste = calc_waste_for_project(p.id, db) # 只用中期制作产出算进度,不把后期算进去 production_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( Submission.project_id == p.id, Submission.total_seconds > 0, Submission.project_phase == PhaseGroup.PRODUCTION, ).scalar() or 0 target = p.target_total_seconds progress = round(production_secs / target * 100, 1) if target > 0 else 0 is_overdue = ( p.estimated_completion_date and today > p.estimated_completion_date ) project_summaries.append({ "id": p.id, "name": p.name, "project_type": p.project_type.value if hasattr(p.project_type, 'value') else p.project_type, "progress_percent": progress, "target_seconds": target, "submitted_seconds": round(production_secs, 1), "waste_rate": waste.get("waste_rate", 0), "waste_hours": waste.get("total_waste_hours", 0), "is_overdue": bool(is_overdue), "estimated_completion_date": str(p.estimated_completion_date) if p.estimated_completion_date else None, }) # 损耗排行(含废弃项目,废弃项目全部产出记为损耗) waste_ranking = [] total_waste_seconds_all = 0.0 total_waste_hours_all = 0.0 total_target_seconds_all = 0.0 for p in active + completed + abandoned: w = calc_waste_for_project(p.id, db) total_waste_seconds_all += w.get("total_waste_seconds", 0) total_waste_hours_all += w.get("total_waste_hours", 0) total_target_seconds_all += p.target_total_seconds or 0 if w.get("total_waste_seconds", 0) > 0 or w.get("total_waste_hours", 0) > 0: waste_ranking.append({ "project_id": p.id, "project_name": p.name, "waste_seconds": w["total_waste_seconds"], "waste_hours": w.get("total_waste_hours", 0), "waste_rate": w["waste_rate"], }) waste_ranking.sort(key=lambda x: x["waste_rate"], reverse=True) total_waste_rate = round(total_waste_seconds_all / total_target_seconds_all * 100, 1) if total_target_seconds_all > 0 else 0 # 已结算项目 settled = [] for p in completed: settlement = calc_project_settlement(p.id, db) settled.append({ "project_id": p.id, "project_name": p.name, "project_type": settlement.get("project_type", ""), "total_cost": settlement.get("total_cost", 0), "contract_amount": settlement.get("contract_amount"), "profit_loss": settlement.get("profit_loss"), }) # ── 图表数据:近30天每日产出趋势(只算中期动画制作) ── daily_trend = [] for i in range(29, -1, -1): d = today - timedelta(days=i) day_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( Submission.submit_date == d, Submission.total_seconds > 0, Submission.project_phase == PhaseGroup.PRODUCTION, ).scalar() or 0 daily_trend.append({ "date": str(d), "seconds": round(day_secs, 1), }) # ── 图表数据:成本构成 ── total_labor_all = 0.0 total_ai_all = 0.0 total_outsource_all = 0.0 total_overhead_all = 0.0 total_management_all = 0.0 for p in active + completed + abandoned: total_labor_all += calc_labor_cost_for_project(p.id, db) total_ai_all += calc_ai_tool_cost_for_project(p.id, db) total_outsource_all += calc_outsource_cost_for_project(p.id, db) total_overhead_all += calc_overhead_cost_for_project(p.id, db) total_management_all += calc_management_cost_for_project(p.id, db) cost_breakdown = [ {"name": "人力成本", "value": round(total_labor_all, 0)}, {"name": "AI工具", "value": round(total_ai_all, 0)}, {"name": "外包", "value": round(total_outsource_all, 0)}, {"name": "固定开支", "value": round(total_overhead_all, 0)}, {"name": "管理成本", "value": round(total_management_all, 0)}, ] # ── 图表数据:各项目产出对比(进行中项目,只算中期动画制作) ── project_comparison = [] for p in active: total_secs_p = db.query(sa_func.sum(Submission.total_seconds)).filter( Submission.project_id == p.id, Submission.total_seconds > 0, Submission.project_phase == PhaseGroup.PRODUCTION, ).scalar() or 0 project_comparison.append({ "name": p.name, "submitted": round(total_secs_p, 0), "target": p.target_total_seconds, }) # ── 盈利概览 ── total_contract = 0.0 total_cost_completed = 0.0 for s in settled: if s.get("contract_amount"): total_contract += s["contract_amount"] total_cost_completed += s.get("total_cost", 0) total_profit = total_contract - total_cost_completed profit_rate = round(total_profit / total_contract * 100, 1) if total_contract > 0 else 0 # 进行中项目的合同额和当前成本 in_progress_contract = 0.0 in_progress_cost = 0.0 for p in active: if p.contract_amount: in_progress_contract += p.contract_amount in_progress_cost += calc_labor_cost_for_project(p.id, db) + calc_ai_tool_cost_for_project(p.id, db) + calc_outsource_cost_for_project(p.id, db) + calc_overhead_cost_for_project(p.id, db) + calc_management_cost_for_project(p.id, db) # 每个项目的盈亏(用于柱状图) profit_by_project = [] for s in settled: if s.get("contract_amount") and s["contract_amount"] > 0: profit_by_project.append({ "name": s["project_name"], "profit": round((s.get("contract_amount", 0) or 0) - s.get("total_cost", 0), 0), }) profitability = { "total_contract": round(total_contract, 0), "total_cost": round(total_cost_completed, 0), "total_profit": round(total_profit, 0), "profit_rate": profit_rate, "in_progress_contract": round(in_progress_contract, 0), "in_progress_cost": round(in_progress_cost, 0), "profit_by_project": profit_by_project, } # ── 风险预警 ── from services.report_service import analyze_project_risks risk_alerts = analyze_project_risks(db) # ── 今日提交情况(排除豁免角色) ── from models import Role exempt_role_ids = set( r.id for r in db.query(Role).filter(Role.exempt_submission == 1).all() ) all_active_users = [ u for u in db.query(User).filter(User.is_active == 1).all() if u.role_id not in exempt_role_ids ] submitted_user_ids = set( uid for (uid,) in db.query(Submission.user_id).filter( Submission.submit_date == today, ).distinct().all() ) submitted_sufficient = [] # 提交且满8小时 submitted_insufficient = [] # 提交但不足8小时 not_submitted_users = [] for u in all_active_users: info = {"id": u.id, "name": u.name} if u.id in submitted_user_ids: hours = db.query(sa_func.sum(Submission.hours_spent)).filter( Submission.user_id == u.id, Submission.submit_date == today, ).scalar() or 0 info["hours"] = round(hours, 1) if hours >= 8: submitted_sufficient.append(info) else: submitted_insufficient.append(info) else: not_submitted_users.append(info) daily_attendance = { "total": len(all_active_users), "submitted_sufficient_count": len(submitted_sufficient), "submitted_insufficient_count": len(submitted_insufficient), "not_submitted_count": len(not_submitted_users), "submitted_sufficient": submitted_sufficient, "submitted_insufficient": submitted_insufficient, "not_submitted": not_submitted_users, } return { "active_projects": len(active), "completed_projects": len(completed), "monthly_labor_cost": round(monthly_labor, 2), "monthly_ai_tool_cost": round(monthly_ai, 2), "monthly_total_seconds": round(monthly_secs, 1), "avg_daily_seconds_per_person": avg_daily, "projects": project_summaries, "total_waste_seconds": round(total_waste_seconds_all, 0), "total_waste_hours": round(total_waste_hours_all, 0), "total_waste_rate": total_waste_rate, "waste_ranking": waste_ranking, "settled_projects": settled, "profitability": profitability, "risk_alerts": risk_alerts, "daily_attendance": daily_attendance, # 图表数据 "daily_trend": daily_trend, "cost_breakdown": cost_breakdown, "project_comparison": project_comparison, } @router.get("/projects/{project_id}/settlement") def get_settlement( project_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("settlement:view")) ): """项目结算报告""" project = db.query(Project).filter(Project.id == project_id).first() if not project: raise HTTPException(status_code=404, detail="项目不存在") return calc_project_settlement(project_id, db) @router.get("/projects/{project_id}/efficiency") def get_efficiency( project_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("efficiency:view")) ): """项目团队效率数据""" return calc_team_efficiency(project_id, db)