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>
This commit is contained in:
358
app/api/kpi.py
Normal file
358
app/api/kpi.py
Normal file
@@ -0,0 +1,358 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user