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:
2025-12-11 16:20:57 +08:00
commit f810ddc2ea
48 changed files with 4950 additions and 0 deletions

9
app/schemas/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
"""
Pydantic Schemas
"""
from app.schemas.auth import *
from app.schemas.employee import *
from app.schemas.kpi_sheet import *
from app.schemas.kpi_item import *
from app.schemas.notification import *
from app.schemas.common import *

44
app/schemas/auth.py Normal file
View File

@@ -0,0 +1,44 @@
"""
認證 Schemas
"""
from typing import Optional
from pydantic import BaseModel, EmailStr
class LoginRequest(BaseModel):
"""登入請求"""
employee_no: str
password: str
class TokenResponse(BaseModel):
"""Token 回應"""
access_token: str
refresh_token: str
token_type: str = "bearer"
class RefreshTokenRequest(BaseModel):
"""更新 Token 請求"""
refresh_token: str
class UserInfo(BaseModel):
"""使用者資訊"""
id: int
employee_no: str
name: str
email: str
department_id: int
department_name: str
job_title: Optional[str]
role: str
is_manager: bool
is_admin: bool
class Config:
from_attributes = True

37
app/schemas/common.py Normal file
View File

@@ -0,0 +1,37 @@
"""
共用 Schemas
"""
from typing import Generic, TypeVar, Optional, List
from pydantic import BaseModel
T = TypeVar("T")
class ErrorDetail(BaseModel):
"""錯誤詳情"""
code: str
message: str
details: Optional[dict] = None
class ErrorResponse(BaseModel):
"""錯誤回應"""
error: ErrorDetail
class PaginatedResponse(BaseModel, Generic[T]):
"""分頁回應"""
items: List[T]
total: int
page: int
page_size: int
pages: int
class MessageResponse(BaseModel):
"""訊息回應"""
message: str

74
app/schemas/dashboard.py Normal file
View File

@@ -0,0 +1,74 @@
"""
儀表板 Schemas
"""
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel
class ProgressStats(BaseModel):
"""進度統計"""
total: int
draft: int
pending: int
approved: int
self_eval: int
manager_eval: int
completed: int
class DistributionItem(BaseModel):
"""分佈項目"""
label: str
count: int
percentage: float
class TrendItem(BaseModel):
"""趨勢項目"""
period: str
average_score: float
completed_count: int
class DashboardAlertResponse(BaseModel):
"""儀表板警示回應"""
id: int
alert_type: str
severity: str
title: str
description: Optional[str]
related_sheet_id: Optional[int]
related_employee_id: Optional[int]
is_resolved: bool
created_at: datetime
class Config:
from_attributes = True
class DashboardProgressResponse(BaseModel):
"""儀表板進度回應"""
period_code: str
period_name: str
stats: ProgressStats
completion_rate: float
class DashboardDistributionResponse(BaseModel):
"""儀表板分佈回應"""
by_department: List[DistributionItem]
by_status: List[DistributionItem]
by_score_range: List[DistributionItem]
class DashboardTrendsResponse(BaseModel):
"""儀表板趨勢回應"""
trends: List[TrendItem]

87
app/schemas/employee.py Normal file
View File

@@ -0,0 +1,87 @@
"""
員工 Schemas
"""
from datetime import date, datetime
from typing import Optional
from pydantic import BaseModel, EmailStr
class DepartmentBase(BaseModel):
"""部門基本資訊"""
id: int
code: str
name: str
level: str
class DepartmentResponse(DepartmentBase):
"""部門回應"""
parent_id: Optional[int]
is_active: bool
class Config:
from_attributes = True
class EmployeeBase(BaseModel):
"""員工基本資訊"""
id: int
employee_no: str
name: str
email: str
class EmployeeSimple(EmployeeBase):
"""員工簡要資訊"""
job_title: Optional[str]
role: str
class Config:
from_attributes = True
class EmployeeResponse(EmployeeBase):
"""員工回應"""
department_id: int
department: DepartmentBase
manager_id: Optional[int]
manager: Optional[EmployeeBase]
job_title: Optional[str]
role: str
status: str
hire_date: Optional[date]
created_at: datetime
class Config:
from_attributes = True
class EmployeeCreate(BaseModel):
"""建立員工"""
employee_no: str
name: str
email: EmailStr
password: str
department_id: int
manager_id: Optional[int] = None
job_title: Optional[str] = None
role: str = "employee"
hire_date: Optional[date] = None
class EmployeeUpdate(BaseModel):
"""更新員工"""
name: Optional[str] = None
email: Optional[EmailStr] = None
department_id: Optional[int] = None
manager_id: Optional[int] = None
job_title: Optional[str] = None
role: Optional[str] = None
status: Optional[str] = None

74
app/schemas/kpi_item.py Normal file
View File

@@ -0,0 +1,74 @@
"""
KPI 項目 Schemas
"""
from typing import Optional
from pydantic import BaseModel, Field
class KPIItemBase(BaseModel):
"""KPI 項目基本資訊"""
name: str
category: str # financial, customer, internal, learning
weight: int = Field(..., ge=1, le=100, description="權重百分比 (1-100)")
class KPIItemCreate(KPIItemBase):
"""建立 KPI 項目"""
template_id: Optional[int] = None
level0_criteria: Optional[str] = None
level1_criteria: Optional[str] = None
level2_criteria: Optional[str] = None
level3_criteria: Optional[str] = None
level4_criteria: Optional[str] = None
class KPIItemUpdate(BaseModel):
"""更新 KPI 項目"""
name: Optional[str] = None
category: Optional[str] = None
weight: Optional[int] = Field(None, ge=1, le=100)
level0_criteria: Optional[str] = None
level1_criteria: Optional[str] = None
level2_criteria: Optional[str] = None
level3_criteria: Optional[str] = None
level4_criteria: Optional[str] = None
class KPIItemResponse(KPIItemBase):
"""KPI 項目回應"""
id: int
sheet_id: int
template_id: Optional[int]
sort_order: int
level0_criteria: Optional[str]
level1_criteria: Optional[str]
level2_criteria: Optional[str]
level3_criteria: Optional[str]
level4_criteria: Optional[str]
self_eval_level: Optional[int]
self_eval_note: Optional[str]
final_level: Optional[int]
final_note: Optional[str]
class Config:
from_attributes = True
class SelfEvalItem(BaseModel):
"""自評項目"""
id: int
level: int = Field(..., ge=0, le=4, description="自評等級 (0-4)")
note: Optional[str] = None
class ManagerEvalItem(BaseModel):
"""主管評核項目"""
id: int
level: int = Field(..., ge=0, le=4, description="評核等級 (0-4)")
note: Optional[str] = None

163
app/schemas/kpi_sheet.py Normal file
View File

@@ -0,0 +1,163 @@
"""
KPI 表單 Schemas
"""
from datetime import datetime
from decimal import Decimal
from typing import Optional, List
from pydantic import BaseModel
from app.schemas.employee import EmployeeSimple
from app.schemas.kpi_item import KPIItemCreate, KPIItemResponse, SelfEvalItem, ManagerEvalItem
class KPIPeriodBase(BaseModel):
"""KPI 期間基本資訊"""
id: int
code: str
name: str
class KPIPeriodResponse(KPIPeriodBase):
"""KPI 期間回應"""
start_date: str
end_date: str
setting_start: str
setting_end: str
self_eval_start: Optional[str]
self_eval_end: Optional[str]
manager_eval_start: Optional[str]
manager_eval_end: Optional[str]
status: str
class Config:
from_attributes = True
class KPISheetCreate(BaseModel):
"""建立 KPI 表單"""
period_id: int
items: List[KPIItemCreate]
class KPISheetUpdate(BaseModel):
"""更新 KPI 表單"""
items: Optional[List[KPIItemCreate]] = None
class KPISheetResponse(BaseModel):
"""KPI 表單回應"""
id: int
employee: EmployeeSimple
period: KPIPeriodBase
department_id: int
status: str
items: List[KPIItemResponse]
# 提交資訊
submitted_at: Optional[datetime]
# 審核資訊
approved_by: Optional[int]
approved_at: Optional[datetime]
approve_comment: Optional[str]
# 退回資訊
rejected_by: Optional[int]
rejected_at: Optional[datetime]
reject_reason: Optional[str]
# 自評資訊
self_eval_at: Optional[datetime]
# 主管評核資訊
manager_eval_by: Optional[int]
manager_eval_at: Optional[datetime]
manager_eval_comment: Optional[str]
# 分數
total_score: Optional[Decimal]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class KPISheetListItem(BaseModel):
"""KPI 表單列表項目"""
id: int
employee: EmployeeSimple
period: KPIPeriodBase
status: str
total_score: Optional[Decimal]
submitted_at: Optional[datetime]
created_at: datetime
class Config:
from_attributes = True
class ApproveRequest(BaseModel):
"""審核通過請求"""
comment: Optional[str] = None
class RejectRequest(BaseModel):
"""退回請求"""
reason: str
class SelfEvalRequest(BaseModel):
"""自評請求"""
items: List[SelfEvalItem]
class ManagerEvalRequest(BaseModel):
"""主管評核請求"""
items: List[ManagerEvalItem]
comment: Optional[str] = None
# KPI 範本相關
class KPITemplateResponse(BaseModel):
"""KPI 範本回應"""
id: int
code: str
name: str
category: str
description: Optional[str]
default_weight: int
level0_desc: str
level1_desc: str
level2_desc: str
level3_desc: str
level4_desc: str
is_active: bool
class Config:
from_attributes = True
class KPIPresetResponse(BaseModel):
"""KPI 預設組合回應"""
id: int
code: str
name: str
description: Optional[str]
items: List[KPITemplateResponse]
class Config:
from_attributes = True

View File

@@ -0,0 +1,47 @@
"""
通知 Schemas
"""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel
class NotificationResponse(BaseModel):
"""通知回應"""
id: int
type: str
title: str
content: Optional[str]
related_sheet_id: Optional[int]
is_read: bool
read_at: Optional[datetime]
created_at: datetime
class Config:
from_attributes = True
class NotificationPreferenceResponse(BaseModel):
"""通知偏好回應"""
email_enabled: bool
in_app_enabled: bool
reminder_days_before: int
class Config:
from_attributes = True
class NotificationPreferenceUpdate(BaseModel):
"""更新通知偏好"""
email_enabled: Optional[bool] = None
in_app_enabled: Optional[bool] = None
reminder_days_before: Optional[int] = None
class UnreadCountResponse(BaseModel):
"""未讀數量回應"""
count: int