Files
KPI-management/app/api/kpi.py
DonaldFang 方士碩 f810ddc2ea Initial commit: KPI Management System Backend
Features:
- FastAPI backend with JWT authentication
- MySQL database with SQLAlchemy ORM
- KPI workflow: draft → pending → approved → evaluation → completed
- Ollama LLM API integration for AI features
- Gitea API integration for version control
- Complete API endpoints for KPI, dashboard, notifications

Tables: KPI_D_* prefix naming convention

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 16:20:57 +08:00

359 lines
11 KiB
Python

"""
KPI API
"""
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.api.deps import get_db, get_current_user, get_current_manager
from app.models.employee import Employee
from app.models.kpi_sheet import KPISheet
from app.models.kpi_period import KPIPeriod
from app.models.kpi_template import KPITemplate, KPIPreset
from app.services.kpi_service import KPISheetService
from app.services.notify_service import NotifyService
from app.schemas.kpi_sheet import (
KPISheetCreate,
KPISheetResponse,
KPISheetListItem,
ApproveRequest,
RejectRequest,
SelfEvalRequest,
ManagerEvalRequest,
KPIPeriodResponse,
KPITemplateResponse,
KPIPresetResponse,
)
from app.schemas.common import PaginatedResponse, MessageResponse
router = APIRouter(prefix="/api/kpi", tags=["KPI"])
# ==================== KPI 表單 ====================
@router.get("/sheets", response_model=List[KPISheetListItem])
def list_sheets(
period_id: Optional[int] = Query(None),
department_id: Optional[int] = Query(None),
status: Optional[str] = Query(None),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500),
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_user),
):
"""查詢 KPI 表單清單"""
service = KPISheetService(db)
sheets = service.get_multi(
period_id=period_id,
department_id=department_id,
status=status,
skip=skip,
limit=limit,
)
return sheets
@router.get("/sheets/my", response_model=List[KPISheetListItem])
def get_my_sheets(
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_user),
):
"""取得我的 KPI 表單"""
service = KPISheetService(db)
return service.get_my_sheets(current_user.id)
@router.get("/sheets/pending", response_model=List[KPISheetListItem])
def get_pending_sheets(
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_manager),
):
"""取得待審核的 KPI 表單(主管用)"""
service = KPISheetService(db)
return service.get_pending_for_manager(current_user.id)
@router.post("/sheets", response_model=KPISheetResponse, status_code=status.HTTP_201_CREATED)
def create_sheet(
data: KPISheetCreate,
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_user),
):
"""建立 KPI 表單"""
service = KPISheetService(db)
return service.create(current_user, data)
@router.post("/sheets/copy-from-previous", response_model=KPISheetResponse, status_code=status.HTTP_201_CREATED)
def copy_from_previous(
period_id: int,
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_user),
):
"""從上期複製 KPI"""
service = KPISheetService(db)
return service.copy_from_previous(current_user, period_id)
@router.post("/sheets/apply-preset/{preset_id}", response_model=KPISheetResponse, status_code=status.HTTP_201_CREATED)
def apply_preset(
preset_id: int,
period_id: int,
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_user),
):
"""套用預設組合"""
service = KPISheetService(db)
return service.apply_preset(current_user, period_id, preset_id)
@router.get("/sheets/{sheet_id}", response_model=KPISheetResponse)
def get_sheet(
sheet_id: int,
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_user),
):
"""取得單一 KPI 表單"""
service = KPISheetService(db)
sheet = service.get_by_id(sheet_id)
if not sheet:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "KPI004", "message": "KPI 表單不存在"},
)
return sheet
@router.delete("/sheets/{sheet_id}", response_model=MessageResponse)
def delete_sheet(
sheet_id: int,
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_user),
):
"""刪除 KPI 表單"""
service = KPISheetService(db)
sheet = service.get_by_id(sheet_id)
if not sheet:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "KPI004", "message": "KPI 表單不存在"},
)
if sheet.employee_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "AUTH003", "message": "權限不足"},
)
service.delete(sheet)
return MessageResponse(message="刪除成功")
# ==================== 審核流程 ====================
@router.post("/sheets/{sheet_id}/submit", response_model=KPISheetResponse)
def submit_sheet(
sheet_id: int,
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_user),
):
"""提交 KPI 審核"""
service = KPISheetService(db)
notify_service = NotifyService(db)
sheet = service.get_by_id(sheet_id)
if not sheet:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "KPI004", "message": "KPI 表單不存在"},
)
if sheet.employee_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "AUTH003", "message": "只能提交自己的 KPI"},
)
result = service.submit(sheet, current_user)
notify_service.notify_kpi_submitted(result)
return result
@router.post("/sheets/{sheet_id}/approve", response_model=KPISheetResponse)
def approve_sheet(
sheet_id: int,
data: ApproveRequest,
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_manager),
):
"""審核通過"""
service = KPISheetService(db)
notify_service = NotifyService(db)
sheet = service.get_by_id(sheet_id)
if not sheet:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "KPI004", "message": "KPI 表單不存在"},
)
# 檢查是否為該員工的主管
if sheet.employee.manager_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "AUTH003", "message": "只能審核直屬部屬的 KPI"},
)
result = service.approve(sheet, current_user, data.comment)
notify_service.notify_kpi_approved(result)
return result
@router.post("/sheets/{sheet_id}/reject", response_model=KPISheetResponse)
def reject_sheet(
sheet_id: int,
data: RejectRequest,
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_manager),
):
"""退回"""
service = KPISheetService(db)
notify_service = NotifyService(db)
sheet = service.get_by_id(sheet_id)
if not sheet:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "KPI004", "message": "KPI 表單不存在"},
)
# 檢查是否為該員工的主管
if sheet.employee.manager_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "AUTH003", "message": "只能審核直屬部屬的 KPI"},
)
result = service.reject(sheet, current_user, data.reason)
notify_service.notify_kpi_rejected(result, data.reason)
return result
# ==================== 評核 ====================
@router.post("/sheets/{sheet_id}/self-eval", response_model=KPISheetResponse)
def self_eval(
sheet_id: int,
data: SelfEvalRequest,
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_user),
):
"""員工自評"""
service = KPISheetService(db)
notify_service = NotifyService(db)
sheet = service.get_by_id(sheet_id)
if not sheet:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "KPI004", "message": "KPI 表單不存在"},
)
if sheet.employee_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "AUTH003", "message": "只能自評自己的 KPI"},
)
result = service.self_eval(sheet, current_user, data)
notify_service.notify_self_eval_completed(result)
return result
@router.post("/sheets/{sheet_id}/manager-eval", response_model=KPISheetResponse)
def manager_eval(
sheet_id: int,
data: ManagerEvalRequest,
db: Session = Depends(get_db),
current_user: Employee = Depends(get_current_manager),
):
"""主管評核"""
service = KPISheetService(db)
notify_service = NotifyService(db)
sheet = service.get_by_id(sheet_id)
if not sheet:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "KPI004", "message": "KPI 表單不存在"},
)
# 檢查是否為該員工的主管
if sheet.employee.manager_id != current_user.id and not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={"code": "AUTH003", "message": "只能評核直屬部屬的 KPI"},
)
result = service.manager_eval(sheet, current_user, data)
notify_service.notify_manager_eval_completed(result)
return result
# ==================== 期間 ====================
@router.get("/periods", response_model=List[KPIPeriodResponse])
def list_periods(db: Session = Depends(get_db)):
"""取得 KPI 期間列表"""
periods = db.query(KPIPeriod).order_by(KPIPeriod.start_date.desc()).all()
return periods
@router.get("/periods/current", response_model=KPIPeriodResponse)
def get_current_period(db: Session = Depends(get_db)):
"""取得當前 KPI 期間"""
period = (
db.query(KPIPeriod)
.filter(KPIPeriod.status.in_(["setting", "self_eval", "manager_eval"]))
.order_by(KPIPeriod.start_date.desc())
.first()
)
if not period:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"code": "PERIOD002", "message": "目前沒有進行中的 KPI 期間"},
)
return period
# ==================== 範本 ====================
@router.get("/templates", response_model=List[KPITemplateResponse])
def list_templates(
category: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
"""取得 KPI 範本列表"""
query = db.query(KPITemplate).filter(KPITemplate.is_active == True)
if category:
query = query.filter(KPITemplate.category == category)
return query.all()
@router.get("/presets", response_model=List[KPIPresetResponse])
def list_presets(db: Session = Depends(get_db)):
"""取得 KPI 預設組合列表"""
presets = db.query(KPIPreset).filter(KPIPreset.is_active == True).all()
return presets