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>
359 lines
11 KiB
Python
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
|