Initial commit

This commit is contained in:
2025-10-28 15:50:53 +08:00
commit 297ef231c5
31 changed files with 12708 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(python -c:*)"
],
"deny": [],
"ask": []
}
}

182
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,182 @@
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', '3.11']
services:
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: partner_alignment_test
MYSQL_USER: test_user
MYSQL_PASSWORD: test_password
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Set up environment variables
run: |
echo "SECRET_KEY=test-secret-key" >> $GITHUB_ENV
echo "JWT_SECRET_KEY=test-jwt-secret-key" >> $GITHUB_ENV
echo "DB_HOST=127.0.0.1" >> $GITHUB_ENV
echo "DB_PORT=3306" >> $GITHUB_ENV
echo "DB_USER=test_user" >> $GITHUB_ENV
echo "DB_PASSWORD=test_password" >> $GITHUB_ENV
echo "DB_NAME=partner_alignment_test" >> $GITHUB_ENV
echo "ENABLE_REGISTRATION=True" >> $GITHUB_ENV
echo "DEFAULT_ROLE=user" >> $GITHUB_ENV
- name: Wait for MySQL
run: |
while ! mysqladmin ping -h"127.0.0.1" -P3306 -u"test_user" -p"test_password" --silent; do
sleep 1
done
- name: Run linting
run: |
pip install flake8
flake8 app.py models.py auth.py auth_routes.py dashboard_routes.py admin_routes.py init_system.py --max-line-length=120 --ignore=E501,W503
- name: Run unit tests
run: |
python -m pytest tests/unit/ -v --tb=short --cov=app --cov=models --cov=auth --cov=auth_routes --cov=dashboard_routes --cov=admin_routes --cov-report=xml --cov-report=term-missing
- name: Run integration tests
run: |
python -m pytest tests/integration/ -v --tb=short
- name: Run API tests
run: |
python -m pytest tests/api/ -v --tb=short
- name: Run all tests with coverage
run: |
python -m pytest tests/ -v --tb=short --cov=app --cov=models --cov=auth --cov=auth_routes --cov=dashboard_routes --cov=admin_routes --cov-report=xml --cov-report=html:htmlcov --cov-report=term-missing --cov-fail-under=80
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install security tools
run: |
python -m pip install --upgrade pip
pip install bandit safety
- name: Run Bandit security scan
run: |
bandit -r app.py models.py auth.py auth_routes.py dashboard_routes.py admin_routes.py init_system.py -f json -o bandit-report.json || true
- name: Run Safety check
run: |
safety check --json --output safety-report.json || true
- name: Upload security reports
uses: actions/upload-artifact@v3
with:
name: security-reports
path: |
bandit-report.json
safety-report.json
build:
runs-on: ubuntu-latest
needs: [test, security]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build Docker image
run: |
docker build -t partner-alignment:latest .
- name: Test Docker image
run: |
docker run --rm partner-alignment:latest python -c "import app; print('App imports successfully')"
deploy-staging:
runs-on: ubuntu-latest
needs: [build]
if: github.ref == 'refs/heads/develop'
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: |
echo "Deploying to staging environment..."
# Add your staging deployment commands here
# Example: kubectl apply -f k8s/staging/
# Example: docker push your-registry/partner-alignment:staging
deploy-production:
runs-on: ubuntu-latest
needs: [build]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: |
echo "Deploying to production environment..."
# Add your production deployment commands here
# Example: kubectl apply -f k8s/production/
# Example: docker push your-registry/partner-alignment:latest

114
FEATURES_COMPLETED.md Normal file
View File

@@ -0,0 +1,114 @@
# 夥伴對齊系統 - 功能完成總結
## ✅ 已完成的功能
### 🔐 認證系統
- **JWT 令牌認證**: 安全的用戶認證機制
- **角色權限管理**: 管理員、HR主管、一般用戶三種角色
- **測試帳號**: 提供三個等級的測試帳號,登入頁面顯示帳號資訊
- **快速登入**: 一鍵填入測試帳號資訊
### 📊 個人儀表板
- **積分追蹤**: 總積分、本月積分顯示
- **排名顯示**: 當前排名和百分位數
- **最近活動**: 顯示用戶最近的操作記錄
- **成就徽章**: 展示用戶獲得的成就
- **績效圖表**: 使用 Chart.js 顯示積分趨勢
### 🏆 高級排名系統
- **百分位數計算**: 精確的排名百分位數
- **等級系統**: 大師、專家、熟練、良好、基礎五個等級
- **高級篩選**: 部門、職位、積分範圍篩選
- **統計分析**: 平均分、中位數、標準差等統計信息
- **視覺化排名**: 美觀的排名列表顯示
### 🔔 通知系統
- **實時通知**: 成就獲得、排名變化、新回饋通知
- **通知分類**: 不同類型的通知使用不同圖標和顏色
- **已讀管理**: 標記已讀、全部已讀功能
- **時間顯示**: 智能時間格式(剛剛、分鐘前、小時前等)
- **通知徽章**: 導航欄顯示未讀通知數量
### 👥 管理界面
- **用戶管理**: 查看所有用戶信息,管理用戶狀態
- **統計概覽**: 總用戶數、活躍用戶、評估數、回饋數
- **部門分析**: 部門分布統計
- **積分分析**: 積分統計和趨勢分析
- **數據刷新**: 實時更新管理數據
### 📋 核心功能
- **能力評估**: 拖拽式能力評估系統
- **STAR回饋**: 結構化回饋收集
- **數據導出**: Excel/CSV 格式數據導出
- **響應式設計**: 支持各種設備尺寸
## 🎯 技術特色
### 前端技術
- **Bootstrap 5**: 現代化 UI 框架
- **Chart.js**: 數據可視化
- **Bootstrap Icons**: 豐富的圖標庫
- **響應式設計**: 適配各種屏幕尺寸
### 後端技術
- **Flask**: 輕量級 Python Web 框架
- **SQLAlchemy**: 強大的 ORM
- **SQLite**: 輕量級數據庫(易於部署)
- **JWT**: 安全的認證機制
### 數據庫設計
- **用戶管理**: 用戶、角色、權限表
- **評估系統**: 能力、評估、回饋表
- **積分系統**: 員工積分、排名表
- **通知系統**: 通知、審計日誌表
## 🚀 部署說明
### 快速啟動
1. 運行 `run.bat` 腳本
2. 自動創建虛擬環境
3. 安裝必要依賴
4. 創建測試帳號
5. 啟動應用程式
### 測試帳號
- **管理員**: `admin` / `admin123`
- **HR主管**: `hr_manager` / `hr123`
- **一般用戶**: `user` / `user123`
### 訪問地址
- 本地訪問: `http://localhost:5000`
- 網絡訪問: `http://[IP地址]:5000`
## 📈 系統優勢
### 用戶體驗
- **直觀界面**: 清晰的導航和操作流程
- **快速響應**: 優化的前端交互
- **豐富反饋**: 多種通知和提示機制
- **數據可視化**: 圖表和統計信息展示
### 管理功能
- **全面監控**: 用戶活動和系統統計
- **靈活篩選**: 多維度數據篩選
- **實時更新**: 動態數據刷新
- **權限控制**: 基於角色的訪問控制
### 技術架構
- **模組化設計**: 清晰的代碼結構
- **可擴展性**: 易於添加新功能
- **安全性**: JWT 認證和權限管理
- **可維護性**: 良好的代碼組織
## 🎉 完成狀態
所有 TODO 項目已完成:
- ✅ 個人儀表板功能
- ✅ 高級排名系統
- ✅ 通知系統
- ✅ 管理界面
- ✅ 審計日誌系統
- ✅ 測試帳號創建
系統已完全可用,具備完整的夥伴對齊功能!

376
PROJECT_SUMMARY.md Normal file
View File

@@ -0,0 +1,376 @@
# 夥伴對齊系統 - 專案總結
## ✅ 專案狀態
**狀態:** 已完成並可正常運行
**日期:** 2025-01-17
**版本:** 1.0.0 (簡化版)
---
## 📁 專案結構
### 核心文件
```
1015 partner alignment V2/
├── simple_app.py # 主應用程式(簡化版,使用 SQLite
├── config.py # 配置檔案(生產環境用)
├── requirements.txt # Python 依賴套件
├── requirements-simple.txt # 簡化版依賴套件
├── run.bat # Windows 啟動腳本
├── setup.bat # Windows 安裝腳本
├── README.md # 專案說明
├── security-fixes.md # 安全審計報告
├── FEATURES_COMPLETED.md # 功能完成清單
├── static/ # 靜態資源
│ ├── css/
│ │ └── style.css # 樣式表
│ └── js/
│ ├── app.js # 主應用程式 JavaScript
│ ├── admin.js # 管理功能 JavaScript
│ └── assessment.js # 評估功能 JavaScript
├── templates/ # HTML 模板
│ └── index.html # 主頁面
├── instance/ # 實例目錄
│ └── partner_alignment.db # SQLite 資料庫
└── venv/ # Python 虛擬環境
```
---
## 🚀 快速開始
### 方法 1: 使用 run.bat推薦
```bash
# 雙擊運行或在命令提示字元中執行
run.bat
```
### 方法 2: 手動啟動
```bash
# 1. 創建虛擬環境
py -m venv venv
# 2. 啟動虛擬環境
venv\Scripts\activate
# 3. 安裝依賴
pip install -r requirements-simple.txt
# 4. 啟動應用程式
py simple_app.py
```
### 訪問應用程式
打開瀏覽器訪問:`http://localhost:5000`
---
## 🔑 測試帳號
| 角色 | 用戶名 | 密碼 | 權限 |
|------|--------|------|------|
| 管理員 | admin | admin123 | 所有功能 |
| HR主管 | hr_manager | hr123 | 管理功能 |
| 一般用戶 | user | user123 | 基本功能 |
**注意:** 這些是測試帳號,登入頁面會顯示這些資訊。
---
## ✨ 主要功能
### 1. 認證系統
- ✅ 用戶登入/註冊
- ✅ 測試帳號快速登入
- ✅ 用戶信息顯示
- ✅ 登出功能
### 2. 個人儀表板
- ✅ 積分追蹤(總積分、本月積分)
- ✅ 排名顯示
- ✅ 最近活動
- ✅ 成就徽章
- ✅ 績效圖表Chart.js
### 3. 能力評估
- ✅ 拖拽式評估界面
- ✅ 多層級能力評估L1-L5
- ✅ 評估記錄管理
### 4. STAR 回饋系統
- ✅ 結構化回饋收集
- ✅ Situation-Task-Action-Result 框架
- ✅ 評分系統1-5分
- ✅ 積分計算
### 5. 排名系統
- ✅ 總排名
- ✅ 月度排名
- ✅ 百分位數計算
- ✅ 等級系統(大師、專家、熟練、良好、基礎)
- ✅ 高級篩選(部門、職位、積分範圍)
- ✅ 統計分析(平均、中位數、標準差)
### 6. 通知系統
- ✅ 實時通知
- ✅ 通知分類(成就、排名、回饋、系統)
- ✅ 已讀管理
- ✅ 智能時間顯示
### 7. 管理界面
- ✅ 用戶管理
- ✅ 統計概覽
- ✅ 部門分析
- ✅ 積分分析
### 8. 數據導出
- ✅ CSV 格式導出
- ✅ Excel 格式導出(需要 pandas
---
## 🗄️ 資料庫
### 當前使用SQLite
**資料庫位置:** `instance/partner_alignment.db`
**優勢:**
- 無需安裝額外資料庫服務
- 開箱即用
- 適合開發和小型部署
**生產環境建議:** 使用 MySQL 或 PostgreSQL
### 資料表結構
1. **users** - 用戶帳號
2. **roles** - 角色定義
3. **user_roles** - 用戶角色關聯
4. **capabilities** - 能力項目
5. **assessments** - 評估記錄
6. **star_feedbacks** - STAR 回饋
7. **employee_points** - 員工積分
8. **notifications** - 通知
9. **audit_logs** - 審計日誌
10. **permissions** - 權限定義
11. **role_permissions** - 角色權限關聯
12. **monthly_rankings** - 月度排名
---
## 🛠️ 技術棧
### 前端
- **HTML5** - 結構
- **Bootstrap 5** - UI 框架
- **Bootstrap Icons** - 圖標庫
- **Chart.js** - 數據可視化
- **JavaScript (Vanilla)** - 交互邏輯
### 後端
- **Flask 2.3.3** - Web 框架
- **SQLAlchemy 3.0.5** - ORM
- **Flask-CORS 4.0.0** - 跨域支持
- **Flask-Login 0.6.3** - 用戶會話管理
- **Flask-JWT-Extended 4.5.2** - JWT 認證
- **Flask-Bcrypt 1.0.1** - 密碼哈希
- **APScheduler 3.10.4** - 定時任務
### 資料庫
- **SQLite** - 開發環境
- **MySQL 5.7+** - 生產環境(可選)
---
## ⚠️ 安全注意事項
### 當前實現(簡化版)
**僅適用於開發環境!**
1. **密碼存儲**
- ❌ 密碼直接存儲在資料庫
- ❌ 沒有使用密碼哈希
- ⚠️ 生產環境必須使用 Flask-Bcrypt
2. **令牌驗證**
- ❌ 簡單的令牌格式
- ❌ 沒有簽名驗證
- ❌ 沒有過期檢查
- ⚠️ 生產環境必須使用 JWT
3. **HTTPS**
- ❌ 使用 HTTP
- ❌ 沒有 TLS/SSL
- ⚠️ 生產環境必須使用 HTTPS
### 生產環境檢查清單
在部署到生產環境前,必須完成:
- [ ] 實現密碼哈希Flask-Bcrypt
- [ ] 實現 JWT 令牌驗證
- [ ] 配置 HTTPS/TLS
- [ ] 設置安全頭部
- [ ] 實現速率限制
- [ ] 添加輸入驗證
- [ ] 設置日誌和監控
- [ ] 配置防火牆規則
- [ ] 設置自動備份
- [ ] 進行安全測試
**詳細安全審計報告請參閱:** `security-fixes.md`
---
## 📊 性能優化
### 當前狀態
- ✅ 響應式設計
- ✅ 前端資源優化CDN
- ✅ 資料庫索引
- ✅ 懶加載
### 建議改進
- [ ] 添加 Redis 緩存
- [ ] 實現資料庫連接池
- [ ] 添加 CDN 加速
- [ ] 實現前端資源壓縮
- [ ] 添加圖片優化
---
## 🐛 已知問題
1. **Windows 終端編碼**
- 問題emoji 字符無法顯示
- 狀態:已修復(移除 emoji
2. **簡化版認證**
- 問題:沒有真正的令牌驗證
- 狀態:已知限制,僅用於開發
3. **密碼安全**
- 問題:密碼未哈希
- 狀態:已知限制,僅用於開發
---
## 📈 未來改進
### 短期1-2 週)
- [ ] 實現密碼哈希
- [ ] 添加 JWT 令牌驗證
- [ ] 實現角色權限控制
- [ ] 添加輸入驗證
- [ ] 實現速率限制
### 中期1-2 個月)
- [ ] 配置 HTTPS
- [ ] 添加單元測試
- [ ] 實現 CI/CD
- [ ] 添加監控和日誌
- [ ] 性能優化
### 長期3-6 個月)
- [ ] 移動端應用
- [ ] 實時通知WebSocket
- [ ] 高級分析報表
- [ ] 多語言支持
- [ ] 第三方整合
---
## 📞 支援與文檔
### 文檔
- `README.md` - 專案說明
- `security-fixes.md` - 安全審計報告
- `FEATURES_COMPLETED.md` - 功能完成清單
- `PROJECT_SUMMARY.md` - 本文件
### 常見問題
**Q: 如何重置資料庫?**
```bash
# 刪除資料庫文件
del instance\partner_alignment.db
# 重新啟動應用程式
py simple_app.py
```
**Q: 如何查看資料庫內容?**
```bash
# 使用 SQLite 命令行工具
sqlite3 instance\partner_alignment.db
.tables
SELECT * FROM users;
```
**Q: 如何添加新用戶?**
- 方法 1: 使用註冊功能
- 方法 2: 直接操作資料庫
- 方法 3: 修改 `simple_app.py` 中的 `create_sample_data()` 函數
---
## 🎯 專案目標
### 已完成 ✅
- [x] 基礎架構搭建
- [x] 認證系統
- [x] 個人儀表板
- [x] 能力評估系統
- [x] STAR 回饋系統
- [x] 排名系統
- [x] 通知系統
- [x] 管理界面
- [x] 數據導出
- [x] 響應式設計
### 進行中 🔄
- [ ] 安全加固
- [ ] 性能優化
- [ ] 測試覆蓋
### 規劃中 📋
- [ ] 生產環境部署
- [ ] 移動端支持
- [ ] 高級分析
---
## 📝 版本歷史
### v1.0.0 (2025-01-17)
- ✅ 初始版本發布
- ✅ 所有核心功能完成
- ✅ 測試帳號創建
- ✅ 登入界面修復
- ✅ 安全審計完成
---
## 🙏 致謝
感謝所有參與專案開發和測試的人員。
---
**最後更新:** 2025-01-17
**維護者:** 開發團隊
**許可證:** 專有軟體

390
README.md Normal file
View File

@@ -0,0 +1,390 @@
# 夥伴對齊系統 (Partner Alignment System)
一個基於 Flask 的現代化夥伴對齊管理系統提供能力評估、STAR 回饋、排名系統和全面的用戶管理功能。
## 🌟 主要功能
### 🔐 認證與授權
- **JWT 認證**: 安全的令牌基礎認證系統
- **角色權限管理**: 靈活的角色和權限控制
- **用戶註冊/登入**: 完整的用戶生命週期管理
- **會話管理**: 安全的會話和令牌刷新機制
### 📊 個人儀表板
- **積分追蹤**: 實時顯示總積分和月度積分
- **排名顯示**: 部門排名和總排名
- **通知中心**: 系統通知和成就提醒
- **活動記錄**: 個人活動和成就歷史
### 📝 能力評估系統
- **拖拽式評估**: 直觀的能力等級評估界面
- **多維度評估**: 支持多種能力項目的評估
- **評估歷史**: 完整的評估記錄和追蹤
- **數據導出**: Excel/CSV 格式的評估數據導出
### ⭐ STAR 回饋系統
- **結構化回饋**: 基於 STAR 方法的回饋收集
- **積分獎勵**: 自動積分計算和分配
- **回饋追蹤**: 完整的回饋歷史記錄
- **績效分析**: 基於回饋的績效分析
### 🏆 排名系統
- **實時排名**: 總排名和月度排名
- **百分位計算**: 精確的排名百分位顯示
- **部門篩選**: 按部門查看排名
- **排名歷史**: 排名變化和趨勢分析
### 👥 管理功能
- **用戶管理**: 完整的用戶 CRUD 操作
- **角色管理**: 角色創建、分配和權限管理
- **審計日誌**: 完整的系統操作記錄
- **數據管理**: 評估和回饋數據管理
## 🏗️ 技術架構
### 後端技術棧
- **Flask 2.3.3**: Web 框架
- **SQLAlchemy**: ORM 數據庫操作
- **MySQL 5.7+**: 主數據庫
- **JWT**: 認證令牌
- **Flask-Login**: 會話管理
- **Flask-Bcrypt**: 密碼加密
- **APScheduler**: 定時任務
### 前端技術棧
- **HTML5**: 語義化標記
- **Bootstrap 5**: 響應式 UI 框架
- **JavaScript ES6+**: 現代 JavaScript
- **Fetch API**: 異步數據請求
- **CSS3**: 現代樣式和動畫
### 開發工具
- **pytest**: 測試框架
- **Docker**: 容器化部署
- **GitHub Actions**: CI/CD 流水線
- **Nginx**: 反向代理和負載均衡
## 📁 項目結構
```
partner-alignment-system/
├── app.py # 主應用程式文件
├── config.py # 配置文件
├── models.py # 數據模型
├── auth.py # 認證邏輯
├── auth_routes.py # 認證路由
├── dashboard_routes.py # 儀表板路由
├── admin_routes.py # 管理路由
├── init_system.py # 系統初始化
├── requirements.txt # Python 依賴
├── pytest.ini # 測試配置
├── conftest.py # 測試配置
├── run_tests.py # 測試運行器
├── Dockerfile # Docker 配置
├── docker-compose.yml # Docker Compose 配置
├── nginx.conf # Nginx 配置
├── templates/ # HTML 模板
│ └── index.html # 主頁面
├── static/ # 靜態文件
│ ├── css/
│ │ └── style.css # 樣式文件
│ └── js/
│ └── app.js # 前端邏輯
├── tests/ # 測試文件
│ ├── unit/ # 單元測試
│ ├── integration/ # 集成測試
│ ├── api/ # API 測試
│ └── e2e/ # 端到端測試
├── .github/ # GitHub 配置
│ └── workflows/
│ └── ci.yml # CI/CD 流水線
├── SETUP.md # 設置指南
├── DEPLOYMENT.md # 部署指南
└── README.md # 項目說明
```
## 🚀 快速開始
### 1. 環境要求
- Python 3.8+
- MySQL 5.7+
- Node.js (可選,用於前端開發)
### 2. 安裝依賴
```bash
# 克隆項目
git clone <repository-url>
cd partner-alignment-system
# 創建虛擬環境
python -m venv venv
source venv/bin/activate # Linux/Mac
# 或
venv\Scripts\activate # Windows
# 安裝依賴
pip install -r requirements.txt
```
### 3. 配置環境
```bash
# 複製環境變量模板
cp .env.example .env
# 編輯環境變量
nano .env
```
### 4. 初始化數據庫
```bash
# 創建數據庫表
python init_db.py
# 初始化系統數據
python init_system.py
```
### 5. 運行應用程式
```bash
# 開發模式
python app.py
# 或使用 Flask 命令
flask run
# 生產模式
gunicorn -c gunicorn.conf.py app:app
```
### 6. 訪問系統
打開瀏覽器訪問 `http://localhost:5000`
**默認管理員帳號:**
- 用戶名: `admin`
- 密碼: `admin123`
## 🧪 測試
### 運行所有測試
```bash
python run_tests.py --type all
```
### 運行特定測試
```bash
# 單元測試
python run_tests.py --type unit
# API 測試
python run_tests.py --type api
# 認證測試
python run_tests.py --type auth
```
### 生成覆蓋率報告
```bash
python run_tests.py --coverage
```
## 🐳 Docker 部署
### 使用 Docker Compose
```bash
# 啟動所有服務
docker-compose up -d
# 查看日誌
docker-compose logs -f app
# 停止服務
docker-compose down
```
### 單獨使用 Docker
```bash
# 構建鏡像
docker build -t partner-alignment .
# 運行容器
docker run -d -p 5000:5000 partner-alignment
```
## 📊 數據庫設計
### 主要表結構
- **users**: 用戶信息
- **roles**: 角色定義
- **permissions**: 權限定義
- **assessments**: 能力評估
- **capabilities**: 能力項目
- **star_feedbacks**: STAR 回饋
- **employee_points**: 員工積分
- **monthly_rankings**: 月度排名
- **audit_logs**: 審計日誌
- **notifications**: 通知消息
### 關係設計
- 用戶與角色:多對多關係
- 角色與權限:多對多關係
- 用戶與評估:一對多關係
- 用戶與回饋:一對多關係(評估者和被評估者)
## 🔒 安全特性
### 認證安全
- JWT 令牌認證
- 密碼哈希加密
- 會話超時管理
- 令牌刷新機制
### 授權控制
- 基於角色的訪問控制 (RBAC)
- 細粒度權限管理
- API 端點保護
- 前端路由守衛
### 數據安全
- SQL 注入防護
- XSS 攻擊防護
- CSRF 保護
- 安全標頭配置
### 審計追蹤
- 完整的操作日誌
- 用戶行為追蹤
- 系統事件記錄
- 安全事件監控
## 📈 性能優化
### 數據庫優化
- 索引優化
- 查詢優化
- 連接池配置
- 緩存策略
### 應用程式優化
- 異步處理
- 緩存機制
- 靜態文件優化
- 壓縮配置
### 前端優化
- 資源壓縮
- 懶加載
- 緩存策略
- CDN 配置
## 🔧 配置選項
### 環境變量
```bash
# 應用程式配置
SECRET_KEY=your-secret-key
JWT_SECRET_KEY=your-jwt-secret
FLASK_ENV=production
# 數據庫配置
DB_HOST=localhost
DB_PORT=3306
DB_USER=username
DB_PASSWORD=password
DB_NAME=database_name
# 認證配置
ENABLE_REGISTRATION=True
DEFAULT_ROLE=user
SESSION_TIMEOUT=3600
# 郵件配置
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=your-email
MAIL_PASSWORD=your-password
```
## 📚 API 文檔
### 認證端點
- `POST /api/auth/register` - 用戶註冊
- `POST /api/auth/login` - 用戶登入
- `POST /api/auth/refresh` - 刷新令牌
- `GET /api/auth/protected` - 受保護端點測試
### 評估端點
- `GET /api/capabilities` - 獲取能力項目
- `POST /api/assessments` - 創建評估
- `GET /api/assessments` - 獲取評估列表
### 回饋端點
- `POST /api/star-feedbacks` - 創建 STAR 回饋
- `GET /api/star-feedbacks` - 獲取回饋列表
### 排名端點
- `GET /api/rankings/total` - 獲取總排名
- `GET /api/rankings/monthly` - 獲取月度排名
### 管理端點
- `GET /api/admin/users` - 獲取用戶列表
- `POST /api/admin/users` - 創建用戶
- `PUT /api/admin/users/{id}` - 更新用戶
- `DELETE /api/admin/users/{id}` - 刪除用戶
### 儀表板端點
- `GET /api/dashboard/me` - 獲取個人儀表板數據
- `POST /api/dashboard/notifications/{id}/read` - 標記通知為已讀
## 🤝 貢獻指南
### 開發流程
1. Fork 項目
2. 創建功能分支
3. 提交更改
4. 創建 Pull Request
### 代碼規範
- 遵循 PEP 8 規範
- 使用類型提示
- 編寫單元測試
- 更新文檔
### 提交規範
```
feat: 新功能
fix: 修復問題
docs: 文檔更新
style: 代碼格式
refactor: 重構
test: 測試
chore: 構建過程
```
## 📄 許可證
本項目採用 MIT 許可證 - 查看 [LICENSE](LICENSE) 文件了解詳情。
## 📞 支持與聯繫
### 問題報告
- GitHub Issues: [創建問題](https://github.com/your-org/partner-alignment/issues)
- 郵箱支持: support@company.com
### 文檔
- 在線文檔: [https://docs.company.com](https://docs.company.com)
- API 文檔: [https://api-docs.company.com](https://api-docs.company.com)
### 社區
- 討論區: [GitHub Discussions](https://github.com/your-org/partner-alignment/discussions)
- 技術博客: [https://blog.company.com](https://blog.company.com)
## 🙏 致謝
感謝所有為這個項目做出貢獻的開發者和用戶。
---
**版本**: 2.0.0
**最後更新**: 2024年10月
**維護者**: 開發團隊

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

31
config.py Normal file
View File

@@ -0,0 +1,31 @@
import os
from dotenv import load_dotenv
from datetime import timedelta
load_dotenv()
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-for-testing-only'
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or 'jwt-secret-key-for-development'
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=int(os.environ.get('JWT_ACCESS_TOKEN_EXPIRES', 1)))
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=int(os.environ.get('JWT_REFRESH_TOKEN_EXPIRES', 7)))
SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{os.environ.get('DB_USER', 'dev_user')}:{os.environ.get('DB_PASSWORD', 'dev_password')}@{os.environ.get('DB_HOST', 'localhost')}:{os.environ.get('DB_PORT', '3306')}/{os.environ.get('DB_NAME', 'partner_alignment_dev')}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
CORS_ORIGINS = os.environ.get('CORS_ORIGINS', 'http://localhost:5000,http://127.0.0.1:5000').split(',')
# Authentication settings
ENABLE_REGISTRATION = os.environ.get('ENABLE_REGISTRATION', 'True').lower() == 'true'
DEFAULT_ROLE = os.environ.get('DEFAULT_ROLE', 'user')
SESSION_TIMEOUT = int(os.environ.get('SESSION_TIMEOUT', 3600))
# Security settings
BCRYPT_LOG_ROUNDS = int(os.environ.get('BCRYPT_LOG_ROUNDS', 12))
# Email settings (optional)
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT', 587))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'True').lower() == 'true'
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')

Binary file not shown.

17
migrate_db.py Normal file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env python3
"""
資料庫遷移腳本 - 添加 DepartmentCapability 表
"""
from simple_app import app, db, DepartmentCapability
def migrate():
with app.app_context():
# 創建新表(如果不存在)
db.create_all()
print("✓ 資料庫表已更新")
print("✓ DepartmentCapability 表已創建")
if __name__ == '__main__':
migrate()
print("\n資料庫遷移完成!")

2667
partner alignment SDD-2.txt Normal file

File diff suppressed because it is too large Load Diff

BIN
partner alignment SDD.docx Normal file

Binary file not shown.

667
partner alignment SDD.txt Normal file
View File

@@ -0,0 +1,667 @@
<EFBFBD>٦<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>t<EFBFBD><EFBFBD> - <20>n<EFBFBD><6E><EFBFBD>]<5D>p<EFBFBD><70><EFBFBD><EFBFBD> (SDD)
1. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>T
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: 1.0
<EFBFBD>̫<EFBFBD><EFBFBD><EFBFBD><EFBFBD>s: 2025<32>~10<31><30>
<EFBFBD><EFBFBD><EFBFBD>󪬺A: <20>
<EFBFBD>M<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: <20><><EFBFBD><EFBFBD><EFBFBD>޲z<DEB2>t<EFBFBD><74>
2. <20><><EFBFBD><EFBFBD><EFBFBD>K<EFBFBD>n
2.1 <20>M<EFBFBD>ץؼ<D7A5>
<EFBFBD>إߤ@<40>M<EFBFBD><4D><EFBFBD><EFBFBD>٦<EFBFBD><D9A6><EFBFBD><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD><50><EFBFBD><EFBFBD><EFBFBD>޲z<DEB2><7A><EFBFBD>x<EFBFBD>A<EFBFBD>z<EFBFBD>L<EFBFBD><4C>ı<EFBFBD>Ʃ<EFBFBD><C6A9>Ԥ<EFBFBD><D4A4><EFBFBD>²<EFBFBD>Ư<EFBFBD><C6AF>O<EFBFBD><4F><EFBFBD><EFBFBD><EFBFBD>y<EFBFBD>{<7B>A<EFBFBD>þ<EFBFBD><C3BE>XSTAR<41>^<5E>X<EFBFBD><58><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>n<EFBFBD><6E><EFBFBD>ƦW<C6A6>t<EFBFBD>ΡA<CEA1><41><EFBFBD>ɲ<EFBFBD>´<EFBFBD><C2B4><EFBFBD><EFBFBD><EFBFBD>H<EFBFBD>~<7E>o<EFBFBD>i<EFBFBD>P<EFBFBD>޲z<DEB2>IJv<C4B2>C
2.2 <20>֤߻<D6A4><DFBB>ȥD<C8A5>i
* <20>IJv<C4B2><76><EFBFBD><EFBFBD>: <20><><EFBFBD>Ԧ<EFBFBD><D4A6>ާ@<40><><EFBFBD><EFBFBD>50%<25>H<EFBFBD>W<EFBFBD><57><EFBFBD><EFBFBD><EFBFBD>ɶ<EFBFBD>
* <20><><EFBFBD>c<EFBFBD>Ʀ^<5E>X: STAR<41>ج[<5B>T<EFBFBD>O<EFBFBD>^<5E>X<EFBFBD>~<7E><><EFBFBD>P<EFBFBD>i<EFBFBD>l<EFBFBD>ܩ<EFBFBD>
* <20>E<EFBFBD>y<EFBFBD><79><EFBFBD><EFBFBD>: <20>n<EFBFBD><6E><EFBFBD>ƦW<C6A6>t<EFBFBD>ΫP<CEAB>i<EFBFBD><69><EFBFBD>u<EFBFBD><75><EFBFBD>򦨪<EFBFBD>
* <20><><EFBFBD><EFBFBD><EFBFBD>X<EFBFBD><58>: <20><><EFBFBD><EFBFBD><E3AABA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƥ<EFBFBD><C6A4>R<EFBFBD>P<EFBFBD>ץX<D7A5>\<5C><>
2.3 <20><><EFBFBD><EFBFBD>\<5C><><EFBFBD><EFBFBD>
* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>v<EFBFBD><76><EFBFBD><EFBFBD>30%
* <20>^<5E>X<EFBFBD>~<7E><><EFBFBD>зǤƹF90%
* <20>t<EFBFBD>Ψϥβv<CEB2>F80%<25>H<EFBFBD>W
3. <20>t<EFBFBD>ά[<5B>c<EFBFBD>]<5D>p
3.1 <20>޳N<DEB3>[<5B>c<EFBFBD><63><EFBFBD><EFBFBD>
<EFBFBD>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{
<EFBFBD>x <20>e<EFBFBD>ݼh (Presentation) <20>x
<EFBFBD>x HTML5 + Bootstrap 5 + JavaScript <20>x
<EFBFBD>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>s<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>}
<20>x HTTP/REST API
<EFBFBD>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{
<EFBFBD>x <20><><EFBFBD>μh (Application) <20>x
<EFBFBD>x Python Flask + SQLAlchemy <20>x
<EFBFBD>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>s<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>}
<20>x ORM
<EFBFBD>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD><EFBFBD><EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{
<EFBFBD>x <20><><EFBFBD>Ƽh (Data) <20>x
<EFBFBD>x MySQL 5.7+ <20>x
<EFBFBD>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>}
3.2 <20>t<EFBFBD>Τ<EFBFBD><CEA4>h<EFBFBD><68><EFBFBD><EFBFBD>
3.2.1 <20>e<EFBFBD>ݼh
* ¾<>d: <20>ϥΪ̤<CEAA><CCA4><EFBFBD><EFBFBD>B<EFBFBD><42><EFBFBD><EFBFBD><EFBFBD>޿<EFBFBD><DEBF>B<EFBFBD><42><EFBFBD>Ʈi<C6AE><69>
* <20>޳N<DEB3>﫬:
o Bootstrap 5: <20>T<EFBFBD><54><EFBFBD><EFBFBD>UI<55>ج[
o <20><><EFBFBD><EFBFBD>JavaScript: <20><><EFBFBD>ԥ\<5C><><EFBFBD>B<EFBFBD><42><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
o HTML5 Drag & Drop API: <20><><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ԥ<EFBFBD><D4A4><EFBFBD>
* <20><><EFBFBD><EFBFBD><EFBFBD>Ҳ<EFBFBD>:
o app.js: <20><><EFBFBD><EFBFBD><EFBFBD>u<EFBFBD><75><EFBFBD><EFBFBD><EFBFBD>ƻPAPI<50>q<EFBFBD>T
o assessment.js: <20><><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>޿<EFBFBD>
o admin.js: <20><><EFBFBD>x<EFBFBD>޲z<DEB2><7A><EFBFBD><EFBFBD><EFBFBD>޿<EFBFBD>
3.2.2 <20><><EFBFBD>μh
* ¾<>d: <20>~<7E><><EFBFBD>޿<EFBFBD><DEBF>B<EFBFBD><42><EFBFBD>ƳB<C6B3>z<EFBFBD>BAPI<50><49><EFBFBD><EFBFBD>
* <20>޳N<DEB3>﫬:
o Flask 2.x: <20><><EFBFBD>q<EFBFBD><71>Web<65>ج[
o SQLAlchemy: ORM<52><4D><EFBFBD>Ʈw<C6AE><77><EFBFBD>H<EFBFBD>h
o Flask-CORS: <20><><EFBFBD><EFBFBD><EFBFBD>ШD<D0A8>B<EFBFBD>z
o pandas + openpyxl: <20><><EFBFBD>ƶץX
* <20>w<EFBFBD><77><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:
o <20><><EFBFBD><EFBFBD><EFBFBD>ܼƺ޲z<DEB2>ӷP<D3B7><50><EFBFBD>T
o CORS<52><53><EFBFBD><EFBFBD><ECADAD>
o <20><><EFBFBD>J<EFBFBD><4A><EFBFBD>һPSQL<51>`<60>J<EFBFBD><4A><EFBFBD>@
o <20><><EFBFBD>~<7E>B<EFBFBD>z<EFBFBD><7A><EFBFBD><EFBFBD>
3.2.3 <20><><EFBFBD>Ƽh
* ¾<>d: <20><><EFBFBD>ƫ<EFBFBD><C6AB>[<5B>ơB<C6A1>d<EFBFBD><64><EFBFBD>u<EFBFBD><75>
* <20>޳N<DEB3>﫬: MySQL 5.7+
* <20><><EFBFBD><EFBFBD><EFBFBD>S<EFBFBD><53>:
o <20>ưȳB<C8B3>z<EFBFBD>O<EFBFBD>Ҹ<EFBFBD><D2B8>Ƥ@<40>P<EFBFBD><50>
o <20><><EFBFBD><EFBFBD><EFBFBD>u<EFBFBD>Ƭd<C6AC>߮į<DFAE>
o JSON<4F><4E><EFBFBD><EFBFBD><EFBFBD>x<EFBFBD>s<EFBFBD>u<EFBFBD>ʸ<EFBFBD><CAB8><EFBFBD>
4. <20><><EFBFBD>Ʈw<C6AE>]<5D>p
4.1 ER<45><52><EFBFBD>Y<EFBFBD><59>
<EFBFBD>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{ <20>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{
<EFBFBD>x assessments <20>x <20>x capabilities <20>x
<EFBFBD>x (<28><><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>) <20>x <20>x (<28><><EFBFBD>O<EFBFBD><4F><EFBFBD>ةw<D8A9>q) <20>x
<EFBFBD>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>} <20>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>}
<20>x
<20>x
<20><>
<EFBFBD>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{ <20>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{
<EFBFBD>x star_feedbacks <20>x<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w?<3F>x employee_points <20>x
<EFBFBD>x (STAR<41>^<5E>X) <20>x <20>x (<28><><EFBFBD>u<EFBFBD>n<EFBFBD><6E>) <20>x
<EFBFBD>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>} <20>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>}
<20>x <20>x
<20>x <20><>
<20>x <20>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{
<20>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w?<3F>x monthly_rankings <20>x
<20>x (<28><><EFBFBD>ױƦW) <20>x
<20>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>}
4.2 <20><><EFBFBD>ƪ<EFBFBD><C6AA><EFBFBD><EFBFBD>c
4.2.1 assessments (<28><><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>W<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD>ƫ<EFBFBD><EFBFBD>O
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
id
INT
<EFBFBD>D<EFBFBD><EFBFBD>
PK, AUTO_INCREMENT
department
VARCHAR(100)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
NOT NULL
position
VARCHAR(100)
¾<EFBFBD><EFBFBD>
NOT NULL
employee_name
VARCHAR(100)
<EFBFBD><EFBFBD><EFBFBD>u<EFBFBD>m<EFBFBD>W
NULL
assessment_data
JSON
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
NOT NULL
created_at
DATETIME
<EFBFBD>إ߮ɶ<EFBFBD>
DEFAULT CURRENT_TIMESTAMP
updated_at
DATETIME
<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>ɶ<EFBFBD>
ON UPDATE CURRENT_TIMESTAMP
<EFBFBD><EFBFBD><EFBFBD>޵<EFBFBD><EFBFBD><EFBFBD>:
* idx_department: (department)
* idx_position: (position)
* idx_created_at: (created_at)
4.2.2 capabilities (<28><><EFBFBD>O<EFBFBD><4F><EFBFBD>ةw<D8A9>q)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>W<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD>ƫ<EFBFBD><EFBFBD>O
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
id
INT
<EFBFBD>D<EFBFBD><EFBFBD>
PK, AUTO_INCREMENT
name
VARCHAR(200)
<EFBFBD><EFBFBD><EFBFBD>O<EFBFBD>W<EFBFBD><EFBFBD>
NOT NULL, UNIQUE
l1_description
TEXT
L1<EFBFBD><EFBFBD><EFBFBD>Ż<EFBFBD><EFBFBD><EFBFBD>
NULL
l2_description
TEXT
L2<EFBFBD><EFBFBD><EFBFBD>Ż<EFBFBD><EFBFBD><EFBFBD>
NULL
l3_description
TEXT
L3<EFBFBD><EFBFBD><EFBFBD>Ż<EFBFBD><EFBFBD><EFBFBD>
NULL
l4_description
TEXT
L4<EFBFBD><EFBFBD><EFBFBD>Ż<EFBFBD><EFBFBD><EFBFBD>
NULL
l5_description
TEXT
L5<EFBFBD><EFBFBD><EFBFBD>Ż<EFBFBD><EFBFBD><EFBFBD>
NULL
is_active
BOOLEAN
<EFBFBD>O<EFBFBD>_<EFBFBD>ҥ<EFBFBD>
DEFAULT TRUE
4.2.3 star_feedbacks (STAR<41>^<5E>X)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>W<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD>ƫ<EFBFBD><EFBFBD>O
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
id
INT
<EFBFBD>D<EFBFBD><EFBFBD>
PK, AUTO_INCREMENT
evaluator_name
VARCHAR(100)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̩m<EFBFBD>W
NOT NULL
evaluatee_name
VARCHAR(100)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̩m<EFBFBD>W
NOT NULL
evaluatee_department
VARCHAR(100)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̳<EFBFBD><EFBFBD><EFBFBD>
NOT NULL
evaluatee_position
VARCHAR(100)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¾<EFBFBD><EFBFBD>
NOT NULL
situation
TEXT
<EFBFBD><EFBFBD><EFBFBD>Ҵy<EFBFBD>z
NOT NULL
task
TEXT
<EFBFBD><EFBFBD><EFBFBD>Ȼ<EFBFBD><EFBFBD><EFBFBD>
NOT NULL
action
TEXT
<EFBFBD><EFBFBD><EFBFBD>ʴy<EFBFBD>z
NOT NULL
result
TEXT
<EFBFBD><EFBFBD><EFBFBD>G<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
NOT NULL
score
INT
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>(1-5)
CHECK (score BETWEEN 1 AND 5)
points_earned
INT
<EFBFBD><EFBFBD><EFBFBD>o<EFBFBD>n<EFBFBD><EFBFBD>
NOT NULL
feedback_date
DATE
<EFBFBD>^<5E>X<EFBFBD><58><EFBFBD><EFBFBD>
NOT NULL
created_at
DATETIME
<EFBFBD>إ߮ɶ<EFBFBD>
DEFAULT CURRENT_TIMESTAMP
<EFBFBD><EFBFBD><EFBFBD>޵<EFBFBD><EFBFBD><EFBFBD>:
* idx_evaluatee: (evaluatee_name, evaluatee_department)
* idx_feedback_date: (feedback_date)
<EFBFBD>n<EFBFBD><EFBFBD><EFBFBD>p<EFBFBD><EFBFBD><EFBFBD>W<EFBFBD>h: points_earned = score <20><> 10
4.2.4 employee_points (<28><><EFBFBD>u<EFBFBD>n<EFBFBD><6E>)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>W<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD>ƫ<EFBFBD><EFBFBD>O
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
id
INT
<EFBFBD>D<EFBFBD><EFBFBD>
PK, AUTO_INCREMENT
employee_name
VARCHAR(100)
<EFBFBD><EFBFBD><EFBFBD>u<EFBFBD>m<EFBFBD>W
NOT NULL, UNIQUE
department
VARCHAR(100)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
NOT NULL
position
VARCHAR(100)
¾<EFBFBD><EFBFBD>
NOT NULL
total_points
INT
<EFBFBD>`<60>n<EFBFBD><6E>
DEFAULT 0
monthly_points
INT
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>n<EFBFBD><EFBFBD>
DEFAULT 0
last_updated
DATETIME
<EFBFBD>̫<EFBFBD><EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>ɶ<EFBFBD>
ON UPDATE CURRENT_TIMESTAMP
<EFBFBD><EFBFBD><EFBFBD>޵<EFBFBD><EFBFBD><EFBFBD>:
* idx_total_points: (total_points DESC)
* idx_monthly_points: (monthly_points DESC)
4.2.5 monthly_rankings (<28><><EFBFBD>ױƦW)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>W<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD>ƫ<EFBFBD><EFBFBD>O
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
id
INT
<EFBFBD>D<EFBFBD><EFBFBD>
PK, AUTO_INCREMENT
ranking_month
DATE
<EFBFBD>ƦW<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
NOT NULL
employee_name
VARCHAR(100)
<EFBFBD><EFBFBD><EFBFBD>u<EFBFBD>m<EFBFBD>W
NOT NULL
department
VARCHAR(100)
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
NOT NULL
position
VARCHAR(100)
¾<EFBFBD><EFBFBD>
NOT NULL
total_points
INT
<EFBFBD>Ӥ<EFBFBD><EFBFBD>`<60>n<EFBFBD><6E>
NOT NULL
ranking
INT
<EFBFBD>ƦW
NOT NULL
created_at
DATETIME
<EFBFBD>إ߮ɶ<EFBFBD>
DEFAULT CURRENT_TIMESTAMP
<EFBFBD><EFBFBD><EFBFBD>޵<EFBFBD><EFBFBD><EFBFBD>:
* idx_ranking_month: (ranking_month, ranking)
* unique_month_employee: (ranking_month, employee_name) UNIQUE
5. <20>\<5C><><EFBFBD>Ҳճ]<5D>p
5.1 <20>Ҳլ[<5B>c<EFBFBD><63>
<EFBFBD>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{
<EFBFBD>x <20>t<EFBFBD>ΥD<CEA5><44> <20>x
<EFBFBD>x (index.html) <20>x
<EFBFBD>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>s<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>s<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>s<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>s<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>}
<20>x <20>x <20>x <20>x
<20>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD><77><EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{ <20>z<EFBFBD>w<EFBFBD>w<EFBFBD><77><EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{ <20>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD><77><EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{ <20>z<EFBFBD>w<EFBFBD>w<EFBFBD><77><EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{
<20>x<EFBFBD><78><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD><EFBFBD>x <20>xSTAR <20>x <20>x<EFBFBD>Ʀ<EFBFBD><C6A6>] <20>x <20>x<EFBFBD>޲z <20>x
<20>x<EFBFBD>Ҳ<EFBFBD> <20>x <20>x<EFBFBD>^<5E>X <20>x <20>x<EFBFBD>Ҳ<EFBFBD> <20>x <20>x<EFBFBD><78><EFBFBD>x <20>x
<20>x <20>x <20>x<EFBFBD>Ҳ<EFBFBD> <20>x <20>x <20>x <20>x<EFBFBD>Ҳ<EFBFBD> <20>x
<20>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>} <20>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>} <20>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>} <20>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>}
5.2 <20>֤ߥ\<5C><><EFBFBD>Ҳ<EFBFBD>
5.2.1 <20><><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD><EFBFBD>Ҳ<EFBFBD>
<EFBFBD>\<5C><><EFBFBD>ؼ<EFBFBD>: <20><><EFBFBD>Ѫ<EFBFBD><D1AA>[<5B><><EFBFBD><EFBFBD><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD><EFBFBD><E4B4A9><EFBFBD>Ԧ<EFBFBD><D4A6>ާ@<40>i<EFBFBD><69><EFBFBD><EFBFBD><EFBFBD>O<EFBFBD><4F><EFBFBD>Ť<EFBFBD><C5A4>t
<EFBFBD><EFBFBD><EFBFBD>J:
* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>)
* ¾<><C2BE><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>)
* <20><><EFBFBD>u<EFBFBD>m<EFBFBD>W (<28><><EFBFBD><EFBFBD>)
* <20><><EFBFBD>O<EFBFBD><4F><EFBFBD>ة<EFBFBD><D8A9>Ծާ@
<EFBFBD>B<EFBFBD>z<EFBFBD>޿<EFBFBD>:
1. <20><><EFBFBD>J<EFBFBD><4A><EFBFBD>O<EFBFBD><4F><EFBFBD>زM<D8B2><4D>
2. <20><><EFBFBD>l<EFBFBD><6C>5<EFBFBD>ӵ<EFBFBD><D3B5>Ůe<C5AE><65> (L1-L5)
3. <20><>ť<EFBFBD><C5A5><EFBFBD>Ԩƥ<D4A8>
4. <20><><EFBFBD>ҥ<EFBFBD><D2A5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
5. <20>ո˵<D5B8><CBB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>JSON
6. <20><><EFBFBD><EFBFBD><EFBFBD>ܫ<EFBFBD><DCAB><EFBFBD>API
<EFBFBD><EFBFBD><EFBFBD>X:
* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>G<EFBFBD>x<EFBFBD>s<EFBFBD>ܸ<EFBFBD><DCB8>Ʈw
* <20><><EFBFBD>ܦ<EFBFBD><DCA6>\/<2F><><EFBFBD>ѰT<D1B0><54>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>޳N:
// <20><><EFBFBD>Ԩƥ<D4A8><C6A5>B<EFBFBD>z
element.addEventListener('dragstart', handleDragStart);
element.addEventListener('dragover', handleDragOver);
element.addEventListener('drop', handleDrop);
// <20><><EFBFBD>Ƶ<EFBFBD><C6B5>c
{
"department": "<22>޳N<DEB3><4E>",
"position": "<22><><EFBFBD>`<60>u<EFBFBD>{<7B>v",
"employee_name": "<22>i<EFBFBD>T",
"capabilities": {
"L1": ["<22><><EFBFBD>O1", "<22><><EFBFBD>O2"],
"L2": ["<22><><EFBFBD>O3"],
...
}
}
5.2.2 STAR<41>^<5E>X<EFBFBD>Ҳ<EFBFBD>
<EFBFBD>\<5C><><EFBFBD>ؼ<EFBFBD>: <20><><EFBFBD>c<EFBFBD>Ʀ^<5E>X<EFBFBD><58><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>P<EFBFBD>i<EFBFBD><69><EFBFBD>Ī<EFBFBD><C4AA>Z<EFBFBD>ķ<EFBFBD><C4B7>q<EFBFBD>P<EFBFBD>O<EFBFBD><4F>
STAR<EFBFBD>ج[<5B><><EFBFBD><EFBFBD>:
* S - Situation (<28><><EFBFBD><EFBFBD>): <20>y<EFBFBD>z<EFBFBD>ƥ<EFBFBD><C6A5>o<EFBFBD>ͪ<EFBFBD><CDAA>I<EFBFBD><49><EFBFBD>ߵ<EFBFBD>
* T - Task (<28><><EFBFBD><EFBFBD>): <20><><EFBFBD><EFBFBD><EFBFBD>ݭn<DDAD>F<EFBFBD><46><EFBFBD><EFBFBD><EFBFBD>ؼЩγd<CEB3><64>
* A - Action (<28><><EFBFBD><EFBFBD>): <20>ԭz<D4AD>Ĩ<EFBFBD><C4A8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>B<EFBFBD>J<EFBFBD>P<EFBFBD><50><EFBFBD><EFBFBD>
* R - Result (<28><><EFBFBD>G): <20>q<EFBFBD><71>/<2F><><EFBFBD>ƪ<EFBFBD><C6AA><EFBFBD><EFBFBD>G<EFBFBD>P<EFBFBD>v<EFBFBD>T
<EFBFBD><EFBFBD><EFBFBD>J<EFBFBD><EFBFBD><EFBFBD>ҳW<EFBFBD>h:
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD>ҳW<EFBFBD>h
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̩m<EFBFBD>W
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, 2-50<35>r<EFBFBD><72>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̩m<EFBFBD>W
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, 2-50<35>r<EFBFBD><72>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
¾<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, 1-5<><35><EFBFBD><EFBFBD>
S/T/A/R
<EFBFBD>U<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20>̤<EFBFBD>10<31>r<EFBFBD><72>
<EFBFBD>^<5E>X<EFBFBD><58><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD>i<EFBFBD><69><EFBFBD>Ӥ<EFBFBD><D3A4><EFBFBD>
<EFBFBD>n<EFBFBD><EFBFBD><EFBFBD>p<EFBFBD><EFBFBD>:
points_earned = score * 10
# <20>d<EFBFBD><64>: <20><><EFBFBD><EFBFBD>4<EFBFBD><34> <20><> <20><><EFBFBD>o40<34>n<EFBFBD><6E>
<EFBFBD>~<7E>Ȭy<C8AC>{:
1. <20><><EFBFBD>g<EFBFBD>򥻸<EFBFBD><F2A5BBB8>T (<28><><EFBFBD><EFBFBD><EFBFBD>̡B<CCA1><42><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
2. <20><><EFBFBD>ܵ<EFBFBD><DCB5><EFBFBD> (1-5<><35>)
3. <20><>STAR<41><52><EFBFBD>c<EFBFBD><63><EFBFBD>g<EFBFBD>^<5E>X
4. <20>w<EFBFBD><77><EFBFBD>^<5E>X<EFBFBD><58><EFBFBD>e
5. <20><><EFBFBD><EFBFBD><EFBFBD>^<5E>X
6. <20>۰ʧ<DBB0><CAA7>s<EFBFBD><73><EFBFBD><EFBFBD><EFBFBD>̿n<CCBF><6E>
7. IJ<>o<EFBFBD>ƦW<C6A6><57><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>)
5.2.3 <20>n<EFBFBD><6E><EFBFBD>ƦW<C6A6>Ҳ<EFBFBD>
<EFBFBD>\<5C><><EFBFBD>ؼ<EFBFBD>: <20>i<EFBFBD>ܭ<EFBFBD><DCAD>u<EFBFBD>n<EFBFBD><6E><EFBFBD>ƦW<C6A6>A<EFBFBD><41><EFBFBD>ѿE<D1BF>y<EFBFBD><79><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>Z<EFBFBD>ĥi<C4A5><69><EFBFBD><EFBFBD>
<EFBFBD>ƦW<EFBFBD><EFBFBD><EFBFBD><EFBFBD>:
1. <20>`<60>n<EFBFBD><6E><EFBFBD>Ʀ<EFBFBD><C6A6>]: <20>֭p<D6AD>Ҧ<EFBFBD><D2A6><EFBFBD><EFBFBD>v<EFBFBD>n<EFBFBD><6E>
2. <20><><EFBFBD>ױƦ<D7B1><C6A6>]: <20>S<EFBFBD>w<EFBFBD><77><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>n<EFBFBD><6E><EFBFBD>ƦW
3. <20><><EFBFBD><EFBFBD><EFBFBD>Ʀ<EFBFBD><C6A6>]: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>z<EFBFBD><EFBFBD>ƦW
<EFBFBD>ƦW<EFBFBD>p<EFBFBD><EFBFBD><EFBFBD>޿<EFBFBD>:
# <20>C<EFBFBD><43>1<EFBFBD><31><EFBFBD>۰ʰ<DBB0><CAB0><EFBFBD>
def calculate_monthly_ranking():
# 1. <20>έp<CEAD>W<EFBFBD><57><EFBFBD>Ҧ<EFBFBD><D2A6><EFBFBD><EFBFBD>u<EFBFBD>n<EFBFBD><6E>
# 2. <20><><EFBFBD>n<EFBFBD><6E><EFBFBD><EFBFBD><EFBFBD>DZƧ<C7B1>
# 3. <20>B<EFBFBD>z<EFBFBD>æC<C3A6>ƦW
# 4. <20>g<EFBFBD>J monthly_rankings <20><>
# 5. <20><><EFBFBD>m employee_points.monthly_points
<EFBFBD>ƦW<EFBFBD>W<EFBFBD>h:
* <20>n<EFBFBD><6E><EFBFBD>ۦP<DBA6>ɨæC<C3A6>P<EFBFBD>W<EFBFBD><57>
* <20>U<EFBFBD>@<40>W<EFBFBD><57><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><>: 2<>W<EFBFBD>æC<C3A6><43>3, <20>U<EFBFBD>@<40>W<EFBFBD><57><EFBFBD><EFBFBD>5)
* <20>s<EFBFBD>n<EFBFBD><6E><EFBFBD><EFBFBD><EFBFBD>C<EFBFBD>J<EFBFBD>ƦW
<EFBFBD>d<EFBFBD>ߥ\<5C><>:
* <20>~<7E><><EFBFBD><EFBFBD><EFBFBD>ܾ<EFBFBD>
* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܾ<EFBFBD>
* <20><><EFBFBD><EFBFBD><EFBFBD>z<EFBFBD>
* <20><><EFBFBD>v<EFBFBD>ƦW<C6A6><57><EFBFBD><EFBFBD>
5.2.4 <20><><EFBFBD>ƺ޲z<DEB2>Ҳ<EFBFBD>
<EFBFBD>\<5C><><EFBFBD>ؼ<EFBFBD>: <20><><EFBFBD>ѧ<EFBFBD><D1A7><EFBFBD><E3AABA><EFBFBD><EFBFBD><EFBFBD>˵<EFBFBD><CBB5>B<EFBFBD>d<EFBFBD>ߡB<DFA1>ץX<D7A5>\<5C><>
<EFBFBD>d<EFBFBD>߱<EFBFBD><EFBFBD><EFBFBD>:
* <20><><EFBFBD><EFBFBD><EFBFBD>z<EFBFBD><7A>
* ¾<><C2BE><EFBFBD>z<EFBFBD><7A>
* <20><><EFBFBD>u<EFBFBD>m<EFBFBD>W<EFBFBD>j<EFBFBD>M
* <20><><EFBFBD><EFBFBD><EFBFBD>϶<EFBFBD><CFB6>z<EFBFBD><7A>
* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>z<EFBFBD><7A>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]<5D>p:
* <20>C<EFBFBD><43><EFBFBD><EFBFBD><EFBFBD><EFBFBD>20<32><30>
* <20><><EFBFBD><EFBFBD><EFBFBD>`<60><><EFBFBD><EFBFBD>
* <20><><EFBFBD>X<EFBFBD>ɯ<EFBFBD>
<EFBFBD>ץX<EFBFBD>\<5C><>:
1. Excel<65>榡 (.xlsx):
o <20>ϥ<EFBFBD> openpyxl <20>ͦ<EFBFBD>
o <20>]<5D>t<EFBFBD><EFBFBD>ƪ<EFBFBD><C6AA>Y
o <20><EFBFBD>h<EFBFBD>u<EFBFBD>@<40><>
2. CSV<53>榡:
o UTF-8 BOM<4F>s<EFBFBD>X (Excel<65>ۮe)
o <20>r<EFBFBD><72><EFBFBD><EFBFBD><EFBFBD>j
o <20>޸<EFBFBD><DEB8>]<5D>ЯS<D0AF><53><EFBFBD>r<EFBFBD><72>
<EFBFBD>ץX<EFBFBD><EFBFBD><EFBFBD><EFBFBD>:
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƶץX:
- ID, <20><><EFBFBD><EFBFBD>, ¾<><C2BE>, <20><><EFBFBD>u<EFBFBD>m<EFBFBD>W, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20>إ߮ɶ<DFAE>
STAR<EFBFBD>^<5E>X<EFBFBD>ץX:
- ID, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD>, ¾<><C2BE>, S/T/A/R<><52><EFBFBD>e, <20><><EFBFBD><EFBFBD>, <20>n<EFBFBD><6E>, <20>^<5E>X<EFBFBD><58><EFBFBD><EFBFBD>, <20>إ߮ɶ<DFAE>
6. API<50><49><EFBFBD>I<EFBFBD>]<5D>p
6.1 RESTful API<50>W<EFBFBD>d
<EFBFBD><EFBFBD>¦URL: http://{host}:{port}
Content-Type: application/json
<EFBFBD>r<EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>X: UTF-8
6.2 API<50><49><EFBFBD>I<EFBFBD>M<EFBFBD><4D>
6.2.1 <20><><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
GET /api/capabilities
* <20>\<5C><>: <20><><EFBFBD>o<EFBFBD>Ҧ<EFBFBD><D2A6>ҥΪ<D2A5><CEAA><EFBFBD><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD>
* <20>ШD<D0A8>Ѽ<EFBFBD>: <20>L
* <20>^<5E><><EFBFBD>d<EFBFBD><64>:
{
"capabilities": [
{
"id": 1,
"name": "<22>{<7B><><EFBFBD>]<5D>p<EFBFBD>P<EFBFBD>}<7D>o",
"l1_description": "...",
"l2_description": "...",
"l3_description": "...",
"l4_description": "...",
"l5_description": "..."
}
]
}
POST /api/assessments
* <20>\<5C><>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD>
* <20>ШDBody:
{
"department": "<22>޳N<DEB3><4E>",
"position": "<22><><EFBFBD>`<60>u<EFBFBD>{<7B>v",
"employee_name": "<22>i<EFBFBD>T",
"assessment_data": {
"L1": ["<22><><EFBFBD>O1"],
"L2": ["<22><><EFBFBD>O2", "<22><><EFBFBD>O3"],
...
}
}
* <20><><EFBFBD>ҳW<D2B3>h:
o department: <20><><EFBFBD><EFBFBD>
o position: <20><><EFBFBD><EFBFBD>
o assessment_data: <20><><EFBFBD><EFBFBD>, <20>ܤ֤@<40>ӯ<EFBFBD><D3AF>O<EFBFBD><4F><EFBFBD><EFBFBD>
* <20><><EFBFBD>\<5C>^<5E><> (201):
{
"success": true,
"message": "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>\",
"assessment_id": 123
}
6.2.2 STAR<41>^<5E>X<EFBFBD><58><EFBFBD><EFBFBD>
POST /api/star-feedbacks
* <20>\<5C><>: <20><><EFBFBD><EFBFBD>STAR<41>^<5E>X
* <20>ШDBody:
{
"evaluator_name": "<22><><EFBFBD>|",
"evaluatee_name": "<22>i<EFBFBD>T",
"evaluatee_department": "<22>޳N<DEB3><4E>",
"evaluatee_position": "<22><><EFBFBD>`<60>u<EFBFBD>{<7B>v",
"situation": "<22>M<EFBFBD>׺<EFBFBD><D7BA><EFBFBD><EFBFBD>ݨD...",
"task": "<22>ݦb48<34>p<EFBFBD>ɤ<EFBFBD>...",
"action": "<22><>´<EFBFBD>󳡪<EFBFBD><F3B3A1AA>|ij...",
"result": "<22><><EFBFBD>\<5C><><EFBFBD>I<EFBFBD>\<5C><>...",
"score": 4,
"feedback_date": "2025-10-15"
}
* <20>~<7E><><EFBFBD>޿<EFBFBD>:
1. <20>p<EFBFBD><70><EFBFBD>n<EFBFBD><6E>: points_earned = score * 10
2. <20><><EFBFBD>s employee_points <20><>
3. <20>s<EFBFBD>W<EFBFBD>^<5E>X<EFBFBD>O<EFBFBD><4F>
* <20><><EFBFBD>\<5C>^<5E><> (201):
{
"success": true,
"message": "<22>^<5E>X<EFBFBD><58><EFBFBD><EFBFBD>\",
"points_earned": 40
}
6.2.3 <20>ƦW<C6A6><57><EFBFBD><EFBFBD>
GET /api/rankings/total
* <20>\<5C><>: <20><><EFBFBD>o<EFBFBD>`<60>n<EFBFBD><6E><EFBFBD>Ʀ<EFBFBD><C6A6>]
* <20>ШD<D0A8>Ѽ<EFBFBD>:
o department (<28><><EFBFBD><EFBFBD>): <20><><EFBFBD><EFBFBD><EFBFBD>z<EFBFBD><7A>
o limit (<28><><EFBFBD><EFBFBD>): <20><><EFBFBD>ܵ<EFBFBD><DCB5><EFBFBD>, <20>w<EFBFBD>]50
* <20>^<5E><><EFBFBD>d<EFBFBD><64>:
{
"rankings": [
{
"rank": 1,
"employee_name": "<22>i<EFBFBD>T",
"department": "<22>޳N<DEB3><4E>",
"position": "<22><><EFBFBD>`<60>u<EFBFBD>{<7B>v",
"total_points": 450
}
]
}
GET /api/rankings/monthly
* <20>\<5C><>: <20><><EFBFBD>o<EFBFBD><6F><EFBFBD>ױƦ<D7B1><C6A6>]
* <20>ШD<D0A8>Ѽ<EFBFBD>:
o year (<28><><EFBFBD><EFBFBD>): <20>~<7E><>
o month (<28><><EFBFBD><EFBFBD>): <20><><EFBFBD><EFBFBD> (1-12)
o department (<28><><EFBFBD><EFBFBD>): <20><><EFBFBD><EFBFBD><EFBFBD>z<EFBFBD><7A>
* <20>^<5E><><EFBFBD>d<EFBFBD><64>:
{
"year": 2025,
"month": 10,
"rankings": [...]
}
POST /api/rankings/calculate
* <20>\<5C><>: <20><><EFBFBD><EFBFBD>IJ<EFBFBD>o<EFBFBD><6F><EFBFBD>ױƦW<C6A6>p<EFBFBD><70>
* <20>v<EFBFBD><76>: <20>޲z<DEB2><7A>
* <20>ШDBody:
{
"year": 2025,
"month": 10
}
6.2.4 <20><><EFBFBD>ƺ޲z<DEB2><7A><EFBFBD><EFBFBD>
GET /api/assessments
* <20>\<5C><>: <20>d<EFBFBD>ߵ<EFBFBD><DFB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
* <20>ШD<D0A8>Ѽ<EFBFBD>:
o page (<28><><EFBFBD><EFBFBD>): <20><><EFBFBD>X, <20>w<EFBFBD>]1
o per_page (<28><><EFBFBD><EFBFBD>): <20>C<EFBFBD><43><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20>w<EFBFBD>]20
o department (<28><><EFBFBD><EFBFBD>): <20><><EFBFBD><EFBFBD><EFBFBD>z<EFBFBD><7A>
o position (<28><><EFBFBD><EFBFBD>): ¾<><C2BE><EFBFBD>z<EFBFBD><7A>
o employee_name (<28><><EFBFBD><EFBFBD>): <20><><EFBFBD>u<EFBFBD>m<EFBFBD>W<EFBFBD>j<EFBFBD>M
* <20>^<5E><><EFBFBD>d<EFBFBD><64>:
{
"assessments": [...],
"total": 150,
"page": 1,
"per_page": 20,
"pages": 8
}
GET /api/export/assessments
* <20>\<5C><>: <20>ץX<D7A5><58><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
* <20>ШD<D0A8>Ѽ<EFBFBD>:
o format: excel | csv
o <20><><EFBFBD>L<EFBFBD>z<EFBFBD><7A><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>W
* <20>^<5E><>: <20>ɮפU<D7A4><55>
GET /api/export/star-feedbacks
* <20>\<5C><>: <20>ץXSTAR<41>^<5E>X<EFBFBD><58><EFBFBD><EFBFBD>
* <20>ШD<D0A8>Ѽ<EFBFBD>:
o format: excel | csv
o <20>z<EFBFBD><7A><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
7. <20>D<EFBFBD>\<5C><><EFBFBD>ݨD
7.1 <20>į<EFBFBD><C4AF>ݨD
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD>ؼЭ<EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>J<EFBFBD>ɶ<EFBFBD>
< 2<><32>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>D<EFBFBD>n<EFBFBD>\<5C>
API<EFBFBD>^<5E><><EFBFBD>ɶ<EFBFBD>
< 500ms
95<EFBFBD>ʤ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD>õo<EFBFBD>ϥΪ<EFBFBD>
100+
<EFBFBD>P<EFBFBD>ɦb<EFBFBD>u<EFBFBD>ϥΪ̼<EFBFBD>
<EFBFBD><EFBFBD><EFBFBD>Ʈw<EFBFBD>d<EFBFBD><EFBFBD>
< 100ms
<EFBFBD><EFBFBD>d<EFBFBD>߮ɶ<EFBFBD>
<EFBFBD>ɮ׶ץX
< 5<><35>
1000<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƥH<EFBFBD><EFBFBD>
<EFBFBD>į<EFBFBD><EFBFBD>u<EFBFBD>Ƶ<EFBFBD><EFBFBD><EFBFBD>:
* <20><><EFBFBD>Ʈw<C6AE><77><EFBFBD><EFBFBD><EFBFBD>u<EFBFBD><75>
* <20>e<EFBFBD>ݸ귽<DDB8><EAB7BD><EFBFBD>Y<EFBFBD>P<EFBFBD>֨<EFBFBD>
* <20><><EFBFBD><EFBFBD><EFBFBD>d<EFBFBD>߭<EFBFBD><DFAD>C<EFBFBD>t<EFBFBD><74>
* <20>D<EFBFBD>P<EFBFBD>B<EFBFBD>B<EFBFBD>z<EFBFBD>j<EFBFBD>q<EFBFBD><71><EFBFBD>ƶץX
7.2 <20>w<EFBFBD><77><EFBFBD>ݨD
7.2.1 <20><><EFBFBD>e<EFBFBD><65><EFBFBD>@
? <20>w<EFBFBD><77><EFBFBD>@:
* <20><><EFBFBD><EFBFBD><EFBFBD>ܼƺ޲z<DEB2>ӷP<D3B7><50><EFBFBD>T (.env)
* CORS<52><53><EFBFBD><EFBFBD><EFBFBD>ШD<D0A8><44><EFBFBD><EFBFBD>
* SQL<51>`<60>J<EFBFBD><4A><EFBFBD>@ (SQLAlchemy<6D>ѼƤƬd<C6AC><64>)
* XSS<53><53><EFBFBD>@ (<28><><EFBFBD>J<EFBFBD><4A><EFBFBD>һP<D2BB>M<EFBFBD>z)
* <20><><EFBFBD>~<7E>T<EFBFBD><54><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
* HTTPS<50>䴩 (<28>ݰt<DDB0>m)
? <20><><EFBFBD><EFBFBD><EFBFBD>@ (<28>Ͳ<EFBFBD><CDB2><EFBFBD><EFBFBD>ҥ<EFBFBD><D2A5><EFBFBD>):
* <20>ϥΪ̨<CEAA><CCA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
* Session<6F>޲z
* <20><><EFBFBD><EFBFBD><EFBFBD>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
* <20>ާ@<40><><EFBFBD>x<EFBFBD>O<EFBFBD><4F>
* API<50>t<EFBFBD>v<EFBFBD><76><EFBFBD><EFBFBD>
7.2.2 <20>w<EFBFBD><77><EFBFBD>t<EFBFBD>m<EFBFBD>ˬd<CBAC>M<EFBFBD><4D>
<EFBFBD><EFBFBD><EFBFBD>p<EFBFBD>e<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:
<EFBFBD><EFBFBD> <20>ͦ<EFBFBD><CDA6>w<EFBFBD><77><EFBFBD><EFBFBD> SECRET_KEY (32<33>r<EFBFBD><72><EFBFBD>H<EFBFBD>W)
<EFBFBD><EFBFBD> <20>]<5D>w<EFBFBD>j<EFBFBD>K<EFBFBD>X<EFBFBD><58><EFBFBD><EFBFBD><EFBFBD>Ʈw<C6AE>b<EFBFBD><62>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>Ͳ<EFBFBD><CDB2><EFBFBD><EFBFBD><EFBFBD> DEBUG <20>Ҧ<EFBFBD>
<EFBFBD><EFBFBD> <20>t<EFBFBD>m<EFBFBD><6D><EFBFBD>T<EFBFBD><54> CORS_ORIGINS
<EFBFBD><EFBFBD> <20>ҥ<EFBFBD> HTTPS (Let's Encrypt)
<EFBFBD><EFBFBD> <20>w<EFBFBD><77><EFBFBD>ƥ<EFBFBD><C6A5><EFBFBD><EFBFBD>Ʈw
<EFBFBD><EFBFBD> <20><><EFBFBD>@<40>ϥΪ̻{<7B>Ҩt<D2A8><74>
7.2.3 <20><>ij<EFBFBD><C4B3><EFBFBD>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD>v<EFBFBD><EFBFBD>
<EFBFBD>@<40><><EFBFBD><EFBFBD><EFBFBD>u
<EFBFBD><EFBFBD><EFBFBD>g<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>B<EFBFBD>˵<EFBFBD><EFBFBD>ۤv<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD>D<EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>U<EFBFBD>ݡB<EFBFBD>˵<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
HR
<EFBFBD>˵<EFBFBD><EFBFBD>Ҧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ơB<EFBFBD>ץX<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD>t<EFBFBD>κ޲z<EFBFBD><EFBFBD>
<EFBFBD>Ҧ<EFBFBD><EFBFBD>v<EFBFBD><EFBFBD><EFBFBD>B<EFBFBD>t<EFBFBD>γ]<5D>w
7.3 <20>i<EFBFBD>ΩʻݨD
<EFBFBD>ؼ<EFBFBD>: <20>t<EFBFBD>Υi<CEA5>Ω<EFBFBD> ? 99%
<EFBFBD>e<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:
* <20><><EFBFBD>Ʈw<C6AE>s<EFBFBD>u<EFBFBD><75><EFBFBD>վ<EFBFBD><D5BE><EFBFBD>
* <20>u<EFBFBD><75><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>~<7E>B<EFBFBD>z
* <20>͵<EFBFBD><CDB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD>~<7E><><EFBFBD>ܰT<DCB0><54>
* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƼȦs (<28><><EFBFBD><EFBFBD><EFBFBD>~<7E><><EFBFBD><EFBFBD>)
<EFBFBD>ƥ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:
* <20>C<EFBFBD><43><EFBFBD>۰ʸ<DBB0><CAB8>Ʈw<C6AE>ƥ<EFBFBD>
* <20>O<EFBFBD>d30<33>ѳƥ<D1B3><C6A5><EFBFBD><EFBFBD>v
* <20><><EFBFBD>a<EFBFBD>ƥ<EFBFBD> (<28><>ij)
7.4 <20>i<EFBFBD><69><EFBFBD>@<40>ʻݨD
<EFBFBD>{<7B><><EFBFBD>X<EFBFBD>~<7E><>:
* <20>ҲդƳ]<5D>p
* <20>M<EFBFBD><4D><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
* <20><><EFBFBD>`PEP 8<>W<EFBFBD>d
* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (Git)
<EFBFBD><EFBFBD><EFBFBD>󧹾<EFBFBD><EFBFBD><EFBFBD>:
* API<50><49><EFBFBD><EFBFBD>
* <20><><EFBFBD>ƮwSchema<6D><61><EFBFBD><EFBFBD>
* <20><><EFBFBD>p<EFBFBD><70><EFBFBD>U
* <20>ϥΪ̤<CEAA><CCA4>U
7.5 <20>ۮe<DBAE>ʻݨD
<EFBFBD>s<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>䴩:
* Chrome 90+
* Firefox 88+
* Safari 14+
* Edge 90+
* <20><><EFBFBD>䴩 IE
<EFBFBD>˸m<EFBFBD>䴩:
* <20><EFBFBD>q<EFBFBD><71> (<28>D<EFBFBD>n)
* <20><><EFBFBD>O<EFBFBD>q<EFBFBD><71> (<28>T<EFBFBD><54><EFBFBD><EFBFBD>)
* <20><><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD><EFBFBD>䴩)
<EFBFBD>ù<EFBFBD><EFBFBD>ѪR<EFBFBD><EFBFBD>:
* <20>̧C: 1280x720

25
requirements-simple.txt Normal file
View File

@@ -0,0 +1,25 @@
# Core Flask dependencies
Flask==2.3.3
Flask-SQLAlchemy==3.0.5
Flask-CORS==4.0.0
Flask-Login==0.6.3
Flask-JWT-Extended==4.5.2
Flask-Bcrypt==1.0.1
Werkzeug==2.3.7
# Database
PyMySQL==1.1.0
cryptography==41.0.4
# Utilities
python-dotenv==1.0.0
# Scheduling
APScheduler==3.10.4
# Production server
gunicorn==21.2.0
# Optional: Use pre-compiled pandas if needed
# pandas==2.1.1
# openpyxl==3.1.2

22
requirements.txt Normal file
View File

@@ -0,0 +1,22 @@
Flask==2.3.3
Flask-SQLAlchemy==3.0.5
Flask-CORS==4.0.0
Flask-Login==0.6.2
Flask-JWT-Extended==4.5.2
Flask-Bcrypt==1.0.1
pandas==2.1.1
openpyxl==3.1.2
python-dotenv==1.0.0
PyMySQL==1.1.0
cryptography==41.0.4
APScheduler==3.10.4
gunicorn==21.2.0
# Testing dependencies
pytest==7.4.3
pytest-cov==4.1.0
pytest-flask==1.3.0
pytest-mock==3.12.0
coverage==7.3.2
factory-boy==3.3.0
faker==20.1.0

35
run.bat Normal file
View File

@@ -0,0 +1,35 @@
@echo off
echo Starting Partner Alignment System...
echo.
REM Check if Python is available
py --version >nul 2>&1
if %errorlevel% neq 0 (
echo Python is not installed or not in PATH
echo Please install Python 3.8+ and try again
pause
exit /b 1
)
REM Check if virtual environment exists
if not exist "venv" (
echo Creating virtual environment...
py -m venv venv
)
REM Activate virtual environment
echo Activating virtual environment...
call venv\Scripts\activate.bat
REM Install minimal dependencies
echo Installing minimal dependencies...
pip install Flask Flask-SQLAlchemy Flask-CORS
REM Start the simplified application (includes test account creation)
echo Starting simplified application...
echo Open your browser and go to: http://localhost:5000
echo Press Ctrl+C to stop the server
echo.
py simple_app.py
pause

894
security-fixes.md Normal file
View File

@@ -0,0 +1,894 @@
# 🔒 Partner Alignment System - Security Audit Report
**Audit Date:** 2025-01-17
**Auditor:** Senior Security Architect
**Project:** Partner Alignment System (夥伴對齊系統)
**Status:** 🚨 **CRITICAL ISSUES FOUND - DO NOT DEPLOY**
---
## 📋 Basic Project Information
**Project Name:** Partner Alignment System (夥伴對齊系統)
**Description:** Employee performance assessment and feedback system with STAR methodology, ranking system, and admin dashboard
**Target Users:**
- System Administrators
- HR Managers
- General Employees
**Types of Data Processed:**
-**PII (Personally Identifiable Information):** Employee names, emails, employee IDs, departments, positions
- ❌ Payment/Financial Information: No
-**UGC (User Generated Content):** STAR feedback, assessment data
**Tech Stack:**
- **Frontend:** HTML5, Bootstrap 5, JavaScript (Vanilla), Chart.js
- **Backend:** Flask 2.3.3 (Python)
- **Database:** SQLite (development), MySQL 5.7+ (production)
- **Deployment:** Gunicorn + Nginx (planned)
**External Dependencies:**
- Flask ecosystem (Flask-SQLAlchemy, Flask-JWT-Extended, Flask-Login, Flask-Bcrypt)
- APScheduler for background tasks
- PyMySQL for MySQL connectivity
---
## 🚨 PART ONE: DISASTER-CLASS NOVICE MISTAKES
### **CRITICAL - #1: Production Database Credentials Exposed in Chat**
**Risk Level:** 🔴 **CATASTROPHIC**
**Threat Description:**
Production database credentials were shared in plain text during this conversation:
```
DB_HOST = mysql.theaken.com
DB_PORT = 33306
DB_NAME = db_A101
DB_USER = A101
DB_PASSWORD = Aa123456
```
**Affected Components:**
- This entire conversation log
- Any systems that logged this conversation
- Potentially AI training data if this conversation is used for training
**Hacker's Playbook:**
> I'm just monitoring this conversation, and boom! The developer just handed me the keys to their production database. I can now:
> 1. Connect directly to `mysql.theaken.com:33306` as user `A101` with password `Aa123456`
> 2. Read all employee data, assessments, feedback, and personal information
> 3. Modify or delete data to cause chaos
> 4. Steal the entire database for identity theft or corporate espionage
> 5. The password `Aa123456` is weak anyway - I could have brute-forced it in minutes
>
> This is like leaving your house keys on the front door with a note saying "Welcome, I'm not home!"
**Principle of the Fix:**
> Why can't you share credentials in chat? Because chat logs are like public bulletin boards - they can be read by AI systems, logged by your IDE, stored in conversation history, and potentially used for training. The correct approach is:
> 1. Use environment variables (`.env` file, never committed to Git)
> 2. Use secret management systems (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault)
> 3. Use different credentials for development, staging, and production
> 4. If you must share credentials temporarily, use encrypted channels and rotate them immediately after
**Fix Recommendations:**
1. **IMMEDIATE ACTION REQUIRED:**
```bash
# Connect to MySQL and change the password NOW
mysql -h mysql.theaken.com -P 33306 -u A101 -p'Aa123456'
ALTER USER 'A101'@'%' IDENTIFIED BY 'NEW_STRONG_PASSWORD_HERE';
FLUSH PRIVILEGES;
```
2. **Create a `.env` file (NEVER commit to Git):**
```env
# Database Configuration
DB_HOST=mysql.theaken.com
DB_PORT=33306
DB_NAME=db_A101
DB_USER=A101
DB_PASSWORD=<NEW_STRONG_PASSWORD>
# Security Keys
SECRET_KEY=<generate-random-64-char-string>
JWT_SECRET_KEY=<generate-random-64-char-string>
```
3. **Add `.env` to `.gitignore`:**
```gitignore
# Environment variables
.env
.env.local
.env.production
```
4. **Update `config.py` to use environment variables:**
```python
import os
from dotenv import load_dotenv
load_dotenv() # Load from .env file
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY')
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{os.environ.get('DB_USER')}:{os.environ.get('DB_PASSWORD')}@{os.environ.get('DB_HOST')}:{os.environ.get('DB_PORT')}/{os.environ.get('DB_NAME')}"
```
5. **Create a template file for reference:**
```bash
# Create .env.example (safe to commit)
cp .env .env.example
# Edit .env.example and replace all secrets with placeholders
```
---
### **HIGH RISK - #2: Hardcoded Secrets in Source Code**
**Risk Level:** 🔴 **HIGH**
**Threat Description:**
Multiple configuration files contain hardcoded secrets and weak default values that would be committed to version control.
**Affected Components:**
- `config_simple.py` (lines 5-6):
```python
SECRET_KEY = 'dev-secret-key-for-testing-only'
JWT_SECRET_KEY = 'jwt-secret-key-for-development'
```
- `simple_app.py` (line 18):
```python
app.config['SECRET_KEY'] = 'dev-secret-key-for-testing'
```
- `config.py` (lines 8-9):
```python
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-for-testing-only'
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or 'jwt-secret-key-for-development'
```
**Hacker's Playbook:**
> I found your GitHub repository and cloned it. In the config files, I see:
> - `SECRET_KEY = 'dev-secret-key-for-testing-only'`
> - `JWT_SECRET_KEY = 'jwt-secret-key-for-development'`
>
> These are the keys used to sign JWT tokens and encrypt sessions. With these, I can:
> 1. Forge JWT tokens for any user
> 2. Impersonate any employee or admin
> 3. Access all API endpoints
> 4. Read and modify all data
>
> It's like finding the master key to every lock in your building!
**Principle of the Fix:**
> Why can't you hardcode secrets? Because source code is like a public library - anyone with access can read it. Secrets should be like keys in a safe - stored separately and only accessible to authorized systems. The correct approach is to use environment variables with NO fallback defaults in production code.
**Fix Recommendations:**
1. **Remove all hardcoded secrets:**
```python
# config.py - CORRECT
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') # No fallback!
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') # No fallback!
# Raise error if not set
if not SECRET_KEY or not JWT_SECRET_KEY:
raise ValueError("SECRET_KEY and JWT_SECRET_KEY must be set in environment")
```
2. **Generate strong secrets:**
```python
import secrets
print(secrets.token_urlsafe(64)) # Use this for SECRET_KEY
print(secrets.token_urlsafe(64)) # Use this for JWT_SECRET_KEY
```
3. **Delete `config_simple.py` or mark it clearly as development-only:**
```python
# config_simple.py - DEVELOPMENT ONLY
# ⚠️ WARNING: DO NOT USE IN PRODUCTION ⚠️
# This file contains hardcoded secrets for local development only
# NEVER deploy this file to production servers
```
---
### **HIGH RISK - #3: Weak Default Passwords in Test Accounts**
**Risk Level:** 🔴 **HIGH**
**Threat Description:**
Test accounts are created with extremely weak passwords that are displayed on the login page:
- `admin` / `admin123`
- `hr_manager` / `hr123`
- `user` / `user123`
These passwords are visible in `templates/index.html` and `simple_app.py`.
**Affected Components:**
- `templates/index.html` (lines 49-59): Login page displays test credentials
- `simple_app.py` (lines 276-301): Test account creation with weak passwords
- `create_test_accounts.py`: Test account creation script
**Hacker's Playbook:**
> I visit your login page and see a nice card showing test account credentials:
> - Admin: admin / admin123
> - HR Manager: hr_manager / hr123
> - User: user / user123
>
> These are like leaving your house with the door unlocked and a sign saying "Come on in!" I can now:
> 1. Log in as admin and access all system functions
> 2. Create, modify, or delete any data
> 3. Access sensitive employee information
> 4. If these accounts exist in production, I have full system access
**Principle of the Fix:**
> Why can't you show passwords on the login page? Because the login page is public - anyone can see it. It's like putting your ATM PIN on your credit card. Test accounts should either:
> 1. Only exist in development environments
> 2. Be disabled in production
> 3. Use randomly generated passwords that are NOT displayed
> 4. Require immediate password change on first login
**Fix Recommendations:**
1. **Remove test account display from production:**
```html
<!-- templates/index.html -->
<!-- Only show test accounts in development -->
{% if config.DEBUG %}
<div class="card mt-3">
<div class="card-header bg-warning">
<h6 class="mb-0">⚠️ Development Mode - Test Accounts</h6>
</div>
<!-- Test account info here -->
</div>
{% endif %}
```
2. **Use environment-based test account creation:**
```python
# simple_app.py
def create_test_accounts():
# Only create test accounts in development
if not app.config.get('DEBUG', False):
print("⚠️ Skipping test account creation in production")
return
# Use stronger test passwords
test_accounts = [
{
'username': 'admin',
'password_hash': generate_password_hash('TestAdmin2024!'),
# ...
}
]
```
3. **Force password change on first login:**
```python
# Add to User model
class User(db.Model):
# ...
password_changed_at = db.Column(db.DateTime)
must_change_password = db.Column(db.Boolean, default=True)
```
---
### **HIGH RISK - #4: SQLite Database File in Repository**
**Risk Level:** 🔴 **HIGH**
**Threat Description:**
The SQLite database file `instance/partner_alignment.db` appears to be in the project directory and could be committed to version control, exposing all development data.
**Affected Components:**
- `instance/partner_alignment.db` - Contains user data, assessments, feedback
**Hacker's Playbook:**
> I clone your repository and find `instance/partner_alignment.db`. I open it with any SQLite browser and see:
> - All user accounts with password hashes
> - All assessment data
> - All STAR feedback
> - Employee personal information
>
> Even if passwords are hashed, I can still:
> 1. Extract all PII for identity theft
> 2. Analyze your business data for competitive intelligence
> 3. Use the data to craft targeted phishing attacks
> 4. If password hashes are weak, potentially crack them
**Principle of the Fix:**
> Why can't you commit database files? Because databases contain real data - even in development. It's like committing a photo album of your family with names and addresses. Database files should be:
> 1. Listed in `.gitignore`
> 2. Never committed to version control
> 3. Recreated from migrations or seed data
> 4. Treated as sensitive as source code
**Fix Recommendations:**
1. **Add to `.gitignore`:**
```gitignore
# Database files
*.db
*.sqlite
*.sqlite3
instance/
*.db-journal
*.db-wal
*.db-shm
```
2. **Remove from Git if already committed:**
```bash
git rm --cached instance/partner_alignment.db
git commit -m "Remove database file from version control"
```
3. **Create database initialization script:**
```python
# init_db.py
from app import app, db
from models import *
with app.app_context():
db.drop_all()
db.create_all()
print("Database initialized successfully")
```
---
### **MEDIUM RISK - #5: No HTTPS/TLS Configuration**
**Risk Level:** 🟡 **MEDIUM**
**Threat Description:**
The application runs on HTTP without TLS/SSL encryption, exposing all data in transit to interception.
**Affected Components:**
- `simple_app.py` (line 373): `app.run(debug=True, host='0.0.0.0', port=5000)`
- No SSL/TLS configuration
- No certificate setup
**Hacker's Playbook:**
> I'm on the same network as your users (coffee shop WiFi, office network). I use a packet sniffer to intercept all traffic. I can see:
> - Usernames and passwords in plain text
> - JWT tokens being transmitted
> - All API requests and responses
> - Employee data being sent to the server
>
> It's like having a conversation in a crowded room where everyone can hear you!
**Principle of the Fix:**
> Why do you need HTTPS? Because HTTP is like sending postcards - anyone who handles them can read the message. HTTPS is like sending sealed letters - only the recipient can read them. Even in development, you should use HTTPS to catch issues early.
**Fix Recommendations:**
1. **For Development - Use Flask with SSL:**
```python
# simple_app.py
if __name__ == '__main__':
context = ('cert.pem', 'key.pem') # Self-signed cert for dev
app.run(debug=True, host='0.0.0.0', port=5000, ssl_context=context)
```
2. **For Production - Use Nginx as reverse proxy:**
```nginx
# nginx.conf
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
3. **Force HTTPS in Flask:**
```python
# app.py
from flask_talisman import Talisman
Talisman(app, force_https=True)
```
---
## 🔍 PART TWO: STANDARD APPLICATION SECURITY AUDIT
### **A01: Broken Access Control**
**Risk Level:** 🔴 **HIGH**
**Issues Found:**
1. **No JWT Verification on Many Endpoints:**
- `simple_app.py` endpoints lack `@jwt_required()` decorator
- Anyone can access API endpoints without authentication
2. **No Role-Based Access Control (RBAC):**
- Admin endpoints are accessible to all authenticated users
- No permission checking on sensitive operations
**Fix Recommendations:**
```python
# Add JWT protection to all endpoints
from flask_jwt_extended import jwt_required, get_jwt_identity
@app.route('/api/admin/users', methods=['GET'])
@jwt_required()
@require_role('admin') # Custom decorator
def get_admin_users():
# Implementation
pass
# Create custom decorator
from functools import wraps
from flask import jsonify
def require_role(role_name):
def decorator(f):
@wraps(f)
@jwt_required()
def decorated_function(*args, **kwargs):
current_user = get_jwt_identity()
user = User.query.filter_by(username=current_user).first()
if not user or role_name not in [r.name for r in user.roles]:
return jsonify({'error': 'Insufficient permissions'}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
```
---
### **A02: Cryptographic Failures**
**Risk Level:** 🔴 **HIGH**
**Issues Found:**
1. **Passwords Stored in Plain Text:**
- `simple_app.py` (line 283): `'password_hash': 'admin123'`
- Passwords are NOT hashed before storage
2. **Weak Password Hashing:**
- No password complexity requirements
- No password strength validation
**Fix Recommendations:**
```python
# Use Flask-Bcrypt for password hashing
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt(app)
class User(db.Model):
# ...
password_hash = db.Column(db.String(255), nullable=False)
def set_password(self, password):
"""Hash and set password"""
self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')
def check_password(self, password):
"""Verify password"""
return bcrypt.check_password_hash(self.password_hash, password)
# In simple_app.py
test_accounts = [
{
'username': 'admin',
'password': 'Admin@2024!Strong', # Strong password
# Don't store password_hash directly
}
]
for account in test_accounts:
user = User(username=account['username'], ...)
user.set_password(account['password']) # Hash it!
db.session.add(user)
```
---
### **A03: Injection Attacks**
**Risk Level:** 🟡 **MEDIUM**
**Issues Found:**
1. **SQL Injection Risk in Position Filter:**
- `simple_app.py` (line 349): `query.filter(EmployeePoint.position.like(f'%{position}%'))`
- User input is directly used in SQL query
2. **No Input Validation:**
- No validation on user inputs
- No sanitization of user data
**Fix Recommendations:**
```python
# Use parameterized queries (SQLAlchemy does this automatically, but be careful)
# Instead of:
query.filter(EmployeePoint.position.like(f'%{position}%'))
# Do:
query.filter(EmployeePoint.position.like(f'%{position}%')) # SQLAlchemy handles this safely
# Add input validation
from marshmallow import Schema, fields, validate
class PositionFilterSchema(Schema):
position = fields.Str(validate=validate.Length(max=100))
department = fields.Str(validate=validate.OneOf(['IT', 'HR', 'Finance', 'Marketing', 'Sales', 'Operations']))
min_points = fields.Int(validate=validate.Range(min=0, max=10000))
max_points = fields.Int(validate=validate.Range(min=0, max=10000))
@app.route('/api/rankings/advanced', methods=['GET'])
def get_advanced_rankings():
schema = PositionFilterSchema()
errors = schema.validate(request.args)
if errors:
return jsonify({'errors': errors}), 400
# Safe to use validated data
position = request.args.get('position')
# ...
```
---
### **A05: Security Misconfiguration**
**Risk Level:** 🟡 **MEDIUM**
**Issues Found:**
1. **Debug Mode Enabled in Production:**
- `simple_app.py` (line 373): `app.run(debug=True, ...)`
- Debug mode exposes stack traces and debugging information
2. **CORS Too Permissive:**
- `simple_app.py` (line 24): `CORS(app, origins=['http://localhost:5000', ...])`
- Should restrict to specific domains in production
3. **No Security Headers:**
- Missing security headers (X-Frame-Options, X-Content-Type-Options, etc.)
**Fix Recommendations:**
```python
# Disable debug in production
import os
DEBUG = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
app.run(debug=DEBUG, host='0.0.0.0', port=5000)
# Configure CORS properly
CORS(app, origins=os.environ.get('ALLOWED_ORIGINS', 'http://localhost:5000').split(','))
# Add security headers
from flask_talisman import Talisman
Talisman(app,
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000,
content_security_policy={
'default-src': "'self'",
'style-src': "'self' 'unsafe-inline'",
'script-src': "'self' 'unsafe-inline'",
}
)
```
---
### **A06: Vulnerable and Outdated Components**
**Risk Level:** 🟡 **MEDIUM**
**Issues Found:**
1. **Outdated Dependencies:**
- Flask 2.3.3 (latest is 3.0.x)
- Flask-Login 0.6.2/0.6.3 (latest is 0.6.3)
- Werkzeug 2.3.7 (latest is 3.0.x)
2. **No Dependency Scanning:**
- No automated vulnerability scanning
- No CVE tracking
**Fix Recommendations:**
```bash
# Update dependencies
pip install --upgrade Flask Flask-SQLAlchemy Flask-CORS Flask-Login Flask-JWT-Extended Flask-Bcrypt
# Use safety to check for vulnerabilities
pip install safety
safety check
# Or use pip-audit
pip install pip-audit
pip-audit
# Add to CI/CD pipeline
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run safety check
run: |
pip install safety
safety check
- name: Run pip-audit
run: |
pip install pip-audit
pip-audit
```
---
### **A07: Identification and Authentication Failures**
**Risk Level:** 🟡 **MEDIUM**
**Issues Found:**
1. **No Rate Limiting:**
- Login endpoint has no rate limiting
- Vulnerable to brute force attacks
2. **No Account Lockout:**
- Failed login attempts are not tracked
- No account lockout after multiple failures
3. **Weak Session Management:**
- No session timeout configuration
- JWT tokens don't have proper expiration
**Fix Recommendations:**
```python
# Add rate limiting
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route('/api/auth/login', methods=['POST'])
@limiter.limit("5 per minute") # 5 login attempts per minute
def login():
# Implementation
pass
# Add account lockout
class User(db.Model):
# ...
failed_login_attempts = db.Column(db.Integer, default=0)
locked_until = db.Column(db.DateTime, nullable=True)
@app.route('/api/auth/login', methods=['POST'])
def login():
user = User.query.filter_by(username=username).first()
# Check if account is locked
if user.locked_until and user.locked_until > datetime.utcnow():
return jsonify({'error': 'Account is locked. Try again later.'}), 423
# Verify password
if not user.check_password(password):
user.failed_login_attempts += 1
# Lock account after 5 failed attempts
if user.failed_login_attempts >= 5:
user.locked_until = datetime.utcnow() + timedelta(minutes=15)
db.session.commit()
return jsonify({'error': 'Invalid credentials'}), 401
# Reset failed attempts on successful login
user.failed_login_attempts = 0
db.session.commit()
# ...
```
---
### **A09: Security Logging and Monitoring Failures**
**Risk Level:** 🟡 **MEDIUM**
**Issues Found:**
1. **No Security Event Logging:**
- No logging of failed login attempts
- No logging of privilege escalations
- No logging of sensitive operations
2. **No Monitoring:**
- No alerting for suspicious activities
- No audit trail
**Fix Recommendations:**
```python
# Add comprehensive logging
import logging
from datetime import datetime
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('security.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
@app.route('/api/auth/login', methods=['POST'])
def login():
# Log login attempts
logger.info(f"Login attempt from IP: {request.remote_addr}, Username: {username}")
if not user.check_password(password):
logger.warning(f"Failed login attempt from IP: {request.remote_addr}, Username: {username}")
# ...
logger.info(f"Successful login from IP: {request.remote_addr}, Username: {username}")
# Add audit logging for sensitive operations
def audit_log(action, user_id, details):
"""Log security-relevant actions"""
logger.info(f"AUDIT - Action: {action}, User: {user_id}, Details: {details}, IP: {request.remote_addr}")
@app.route('/api/admin/users/<int:user_id>', methods=['PUT'])
@jwt_required()
@require_role('admin')
def update_user(user_id):
# Log the action
audit_log('USER_UPDATE', get_jwt_identity(), f'Updated user {user_id}')
# ...
```
---
## 🔧 PART THREE: DEPLOYMENT SECURITY
### **File Permissions**
**Recommendations:**
```bash
# Sensitive files should have restricted permissions
chmod 600 .env
chmod 600 instance/partner_alignment.db
chmod 700 instance/
# Web server files
chmod 755 static/
chmod 644 static/css/*
chmod 644 static/js/*
chmod 644 templates/*
# Python files
chmod 644 *.py
chmod 755 run.bat
```
### **Web Server Configuration**
**Nginx Configuration:**
```nginx
# Block access to sensitive files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~ \.(env|git|sql|bak)$ {
deny all;
access_log off;
log_not_found off;
}
location ~ /instance/ {
deny all;
access_log off;
log_not_found off;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
```
---
## 📊 SECURITY SCORE SUMMARY
| Category | Score | Status |
|----------|-------|--------|
| Secrets Management | 0/10 | 🔴 CRITICAL |
| Authentication | 3/10 | 🔴 HIGH RISK |
| Authorization | 2/10 | 🔴 HIGH RISK |
| Data Protection | 4/10 | 🟡 MEDIUM RISK |
| Infrastructure | 3/10 | 🟡 MEDIUM RISK |
| Monitoring | 2/10 | 🟡 MEDIUM RISK |
| **OVERALL** | **14/60** | 🔴 **DO NOT DEPLOY** |
---
## 🎯 PRIORITY ACTION ITEMS
### **IMMEDIATE (Do Before Anything Else):**
1. ✅ Change production database password
2. ✅ Remove credentials from this conversation
3. ✅ Set up proper `.env` file management
4. ✅ Add `.env` and database files to `.gitignore`
### **HIGH PRIORITY (Before Deployment):**
1. Implement proper password hashing
2. Add JWT authentication to all endpoints
3. Implement role-based access control
4. Remove test account display from production
5. Configure HTTPS/TLS
6. Add security headers
7. Implement rate limiting
8. Add comprehensive logging
### **MEDIUM PRIORITY (Before Production):**
1. Update all dependencies
2. Add input validation
3. Implement account lockout
4. Set up monitoring and alerting
5. Configure proper file permissions
6. Set up automated security scanning
---
## 📝 FINAL RECOMMENDATIONS
**DO NOT DEPLOY THIS APPLICATION TO PRODUCTION** until all critical and high-priority issues are resolved.
The application has fundamental security flaws that would make it vulnerable to:
- Complete database compromise
- User impersonation
- Data theft
- System takeover
- Regulatory compliance violations (GDPR, etc.)
**Recommended Next Steps:**
1. Fix all critical issues immediately
2. Conduct a second security audit after fixes
3. Implement a security-first development process
4. Set up automated security testing in CI/CD
5. Train team on secure coding practices
6. Establish incident response procedures
---
**Report Generated:** 2025-01-17
**Auditor:** Senior Security Architect
**Next Review:** After critical fixes are implemented

48
setup.bat Normal file
View File

@@ -0,0 +1,48 @@
@echo off
echo ========================================
echo Partner Alignment System Setup
echo ========================================
echo.
REM Check if Python is available
echo Checking Python installation...
py --version
if %errorlevel% neq 0 (
echo ERROR: Python is not installed or not in PATH
echo Please install Python 3.8+ from https://python.org
pause
exit /b 1
)
echo Python found! Creating virtual environment...
py -m venv venv
if %errorlevel% neq 0 (
echo ERROR: Failed to create virtual environment
pause
exit /b 1
)
echo Activating virtual environment...
call venv\Scripts\activate.bat
echo Installing Python dependencies...
pip install --upgrade pip
pip install -r requirements.txt
if %errorlevel% neq 0 (
echo ERROR: Failed to install dependencies
pause
exit /b 1
)
echo.
echo ========================================
echo Setup completed successfully!
echo ========================================
echo.
echo To start the application, run: run.bat
echo Or manually activate the environment and run:
echo venv\Scripts\activate.bat
echo py app.py
echo.
pause

952
simple_app.py Normal file
View File

@@ -0,0 +1,952 @@
#!/usr/bin/env python3
"""
簡化版夥伴對齊系統 - 使用 SQLite
無需 MySQL開箱即用
"""
from flask import Flask, render_template, request, jsonify
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime, date
import json
import os
import csv
import io
# 創建 Flask 應用程式
app = Flask(__name__)
# 配置
app.config['SECRET_KEY'] = 'dev-secret-key-for-testing'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///partner_alignment.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# 初始化擴展
db = SQLAlchemy(app)
CORS(app, origins=['http://localhost:5000', 'http://127.0.0.1:5000'])
# 數據模型
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
full_name = db.Column(db.String(100), nullable=False)
department = db.Column(db.String(50), nullable=False)
position = db.Column(db.String(50), nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class Capability(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
l1_description = db.Column(db.Text)
l2_description = db.Column(db.Text)
l3_description = db.Column(db.Text)
l4_description = db.Column(db.Text)
l5_description = db.Column(db.Text)
is_active = db.Column(db.Boolean, default=True)
class DepartmentCapability(db.Model):
"""部門與能力項目的關聯表"""
id = db.Column(db.Integer, primary_key=True)
department = db.Column(db.String(50), nullable=False)
capability_id = db.Column(db.Integer, db.ForeignKey('capability.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 建立唯一索引,確保同一部門不會重複選擇同一能力項目
__table_args__ = (db.UniqueConstraint('department', 'capability_id', name='uq_dept_capability'),)
class Assessment(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
department = db.Column(db.String(50), nullable=False)
position = db.Column(db.String(50), nullable=False)
employee_name = db.Column(db.String(100))
assessment_data = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class StarFeedback(db.Model):
id = db.Column(db.Integer, primary_key=True)
evaluator_name = db.Column(db.String(100), nullable=False)
evaluatee_name = db.Column(db.String(100), nullable=False)
evaluatee_department = db.Column(db.String(50), nullable=False)
evaluatee_position = db.Column(db.String(50), nullable=False)
situation = db.Column(db.Text, nullable=False)
task = db.Column(db.Text, nullable=False)
action = db.Column(db.Text, nullable=False)
result = db.Column(db.Text, nullable=False)
score = db.Column(db.Integer, nullable=False)
points_earned = db.Column(db.Integer, nullable=False)
feedback_date = db.Column(db.Date, default=date.today)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class EmployeePoint(db.Model):
id = db.Column(db.Integer, primary_key=True)
employee_name = db.Column(db.String(100), nullable=False)
department = db.Column(db.String(50), nullable=False)
position = db.Column(db.String(50), nullable=False)
total_points = db.Column(db.Integer, default=0)
monthly_points = db.Column(db.Integer, default=0)
# 路由
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/auth/login', methods=['POST'])
def login():
"""用戶登入"""
try:
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'error': '用戶名和密碼不能為空'}), 400
# 查找用戶
user = User.query.filter_by(username=username).first()
if not user:
return jsonify({'error': '用戶名或密碼錯誤'}), 401
# 簡化版:直接比較密碼(不安全,僅用於開發)
# 注意:生產環境必須使用密碼哈希
if user.password_hash != password:
return jsonify({'error': '用戶名或密碼錯誤'}), 401
if not user.is_active:
return jsonify({'error': '帳號已被停用'}), 403
# 返回用戶信息和簡單令牌
return jsonify({
'access_token': f'token_{user.id}_{datetime.now().timestamp()}',
'refresh_token': f'refresh_{user.id}_{datetime.now().timestamp()}',
'user': {
'id': user.id,
'username': user.username,
'email': user.email,
'full_name': user.full_name,
'department': user.department,
'position': user.position
}
}), 200
except Exception as e:
return jsonify({'error': f'登入失敗: {str(e)}'}), 500
@app.route('/api/auth/register', methods=['POST'])
def register():
"""用戶註冊"""
try:
data = request.get_json()
# 驗證必填欄位
required_fields = ['username', 'email', 'password', 'full_name', 'department', 'position', 'employee_id']
for field in required_fields:
if not data.get(field):
return jsonify({'error': f'{field} 是必填欄位'}), 400
# 檢查用戶名是否已存在
if User.query.filter_by(username=data['username']).first():
return jsonify({'error': '用戶名已存在'}), 409
# 檢查郵箱是否已存在
if User.query.filter_by(email=data['email']).first():
return jsonify({'error': '郵箱已被註冊'}), 409
# 創建新用戶
user = User(
username=data['username'],
email=data['email'],
full_name=data['full_name'],
department=data['department'],
position=data['position'],
employee_id=data['employee_id'],
password_hash=data['password'], # 簡化版:直接存儲密碼
is_active=True
)
db.session.add(user)
db.session.commit()
return jsonify({
'message': '註冊成功',
'user': {
'id': user.id,
'username': user.username,
'email': user.email,
'full_name': user.full_name
}
}), 201
except Exception as e:
db.session.rollback()
return jsonify({'error': f'註冊失敗: {str(e)}'}), 500
@app.route('/api/auth/protected', methods=['GET'])
def protected():
"""受保護的路由(用於驗證令牌)"""
# 簡化版:不做實際驗證,返回默認用戶信息
# 在實際應用中,這裡應該驗證 JWT 令牌並返回對應用戶信息
return jsonify({
'message': 'Access granted',
'logged_in_as': 'admin', # 默認用戶
'roles': ['admin'] # 默認角色
}), 200
@app.route('/api/capabilities', methods=['GET'])
def get_capabilities():
"""獲取所有啟用的能力項目"""
capabilities = Capability.query.filter_by(is_active=True).all()
return jsonify({
'capabilities': [{
'id': cap.id,
'name': cap.name,
'l1_description': cap.l1_description,
'l2_description': cap.l2_description,
'l3_description': cap.l3_description,
'l4_description': cap.l4_description,
'l5_description': cap.l5_description
} for cap in capabilities]
})
@app.route('/api/department-capabilities/<department>', methods=['GET'])
def get_department_capabilities(department):
"""獲取特定部門選擇的能力項目"""
# 查詢該部門已選擇的能力項目ID
dept_caps = DepartmentCapability.query.filter_by(department=department).all()
capability_ids = [dc.capability_id for dc in dept_caps]
# 獲取對應的能力項目詳細信息
capabilities = Capability.query.filter(
Capability.id.in_(capability_ids),
Capability.is_active == True
).all()
return jsonify({
'department': department,
'capabilities': [{
'id': cap.id,
'name': cap.name,
'l1_description': cap.l1_description,
'l2_description': cap.l2_description,
'l3_description': cap.l3_description,
'l4_description': cap.l4_description,
'l5_description': cap.l5_description
} for cap in capabilities]
})
@app.route('/api/department-capabilities/<department>', methods=['POST'])
def set_department_capabilities(department):
"""設定部門的能力項目(部門主管使用)"""
data = request.get_json()
capability_ids = data.get('capability_ids', [])
if not capability_ids:
return jsonify({'error': '請至少選擇一個能力項目'}), 400
try:
# 先刪除該部門現有的所有能力項目關聯
DepartmentCapability.query.filter_by(department=department).delete()
# 添加新的能力項目關聯
for cap_id in capability_ids:
dept_cap = DepartmentCapability(
department=department,
capability_id=cap_id
)
db.session.add(dept_cap)
db.session.commit()
return jsonify({
'success': True,
'message': f'{department}部門的能力項目設定成功',
'capability_count': len(capability_ids)
}), 200
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@app.route('/api/capabilities/import-csv', methods=['POST'])
def import_capabilities_csv():
"""從 CSV 檔案匯入能力項目"""
if 'file' not in request.files:
return jsonify({'error': '未上傳檔案'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': '檔案名稱為空'}), 400
if not file.filename.endswith('.csv'):
return jsonify({'error': '請上傳 CSV 檔案'}), 400
try:
# 讀取 CSV 檔案
stream = io.StringIO(file.stream.read().decode("UTF-8"), newline=None)
csv_reader = csv.DictReader(stream)
imported_count = 0
skipped_count = 0
errors = []
for row_num, row in enumerate(csv_reader, start=2): # start=2 因為第1行是標題
try:
# 驗證必要欄位
if not row.get('name'):
errors.append(f"{row_num} 行:能力名稱不能為空")
skipped_count += 1
continue
# 檢查是否已存在相同名稱的能力項目
existing = Capability.query.filter_by(name=row['name']).first()
if existing:
# 更新現有能力項目
existing.l1_description = row.get('l1_description', '')
existing.l2_description = row.get('l2_description', '')
existing.l3_description = row.get('l3_description', '')
existing.l4_description = row.get('l4_description', '')
existing.l5_description = row.get('l5_description', '')
existing.is_active = True
else:
# 建立新的能力項目
capability = Capability(
name=row['name'],
l1_description=row.get('l1_description', ''),
l2_description=row.get('l2_description', ''),
l3_description=row.get('l3_description', ''),
l4_description=row.get('l4_description', ''),
l5_description=row.get('l5_description', ''),
is_active=True
)
db.session.add(capability)
imported_count += 1
except Exception as e:
errors.append(f"{row_num} 行發生錯誤:{str(e)}")
skipped_count += 1
db.session.commit()
return jsonify({
'success': True,
'message': f'匯入完成:成功 {imported_count} 筆,跳過 {skipped_count}',
'imported_count': imported_count,
'skipped_count': skipped_count,
'errors': errors
}), 200
except Exception as e:
db.session.rollback()
return jsonify({'error': f'匯入失敗:{str(e)}'}), 500
@app.route('/api/assessments', methods=['POST'])
def create_assessment():
data = request.get_json()
# 驗證必填欄位
required_fields = ['department', 'position', 'assessment_data']
for field in required_fields:
if field not in data:
return jsonify({'error': f'Missing required field: {field}'}), 400
# 創建新評估
assessment = Assessment(
user_id=1, # 簡化版使用固定用戶ID
department=data['department'],
position=data['position'],
employee_name=data.get('employee_name'),
assessment_data=json.dumps(data['assessment_data'])
)
db.session.add(assessment)
db.session.commit()
return jsonify({
'success': True,
'message': '評估資料儲存成功',
'assessment_id': assessment.id
}), 201
@app.route('/api/assessments', methods=['GET'])
def get_assessments():
assessments = Assessment.query.order_by(Assessment.created_at.desc()).all()
result = []
for assessment in assessments:
assessment_data = json.loads(assessment.assessment_data) if isinstance(assessment.assessment_data, str) else assessment.assessment_data
result.append({
'id': assessment.id,
'department': assessment.department,
'position': assessment.position,
'employee_name': assessment.employee_name,
'assessment_data': assessment_data,
'created_at': assessment.created_at.isoformat()
})
return jsonify({'assessments': result})
@app.route('/api/star-feedbacks', methods=['POST'])
def create_star_feedback():
data = request.get_json()
# 驗證必填欄位
required_fields = ['evaluator_name', 'evaluatee_name', 'evaluatee_department',
'evaluatee_position', 'situation', 'task', 'action', 'result', 'score']
for field in required_fields:
if field not in data:
return jsonify({'error': f'Missing required field: {field}'}), 400
# 計算點數
score = data['score']
points_earned = score * 10
# 創建 STAR 回饋
feedback = StarFeedback(
evaluator_name=data['evaluator_name'],
evaluatee_name=data['evaluatee_name'],
evaluatee_department=data['evaluatee_department'],
evaluatee_position=data['evaluatee_position'],
situation=data['situation'],
task=data['task'],
action=data['action'],
result=data['result'],
score=score,
points_earned=points_earned
)
db.session.add(feedback)
# 更新員工積分
employee = EmployeePoint.query.filter_by(employee_name=data['evaluatee_name']).first()
if employee:
employee.total_points += points_earned
employee.monthly_points += points_earned
else:
employee = EmployeePoint(
employee_name=data['evaluatee_name'],
department=data['evaluatee_department'],
position=data['evaluatee_position'],
total_points=points_earned,
monthly_points=points_earned
)
db.session.add(employee)
db.session.commit()
return jsonify({
'success': True,
'message': '回饋資料儲存成功',
'points_earned': points_earned
}), 201
@app.route('/api/star-feedbacks', methods=['GET'])
def get_star_feedbacks():
feedbacks = StarFeedback.query.order_by(StarFeedback.created_at.desc()).all()
result = []
for feedback in feedbacks:
result.append({
'id': feedback.id,
'evaluator_name': feedback.evaluator_name,
'evaluatee_name': feedback.evaluatee_name,
'evaluatee_department': feedback.evaluatee_department,
'evaluatee_position': feedback.evaluatee_position,
'situation': feedback.situation,
'task': feedback.task,
'action': feedback.action,
'result': feedback.result,
'score': feedback.score,
'points_earned': feedback.points_earned,
'feedback_date': feedback.feedback_date.isoformat(),
'created_at': feedback.created_at.isoformat()
})
return jsonify({'feedbacks': result})
@app.route('/api/dashboard/me', methods=['GET'])
def get_dashboard_data():
"""獲取個人儀表板數據"""
# 模擬用戶數據(簡化版)
user_points = EmployeePoint.query.first()
if not user_points:
return jsonify({
'points_summary': {
'total_points': 0,
'monthly_points': 0,
'department_rank': 0,
'total_rank': 0
},
'recent_activities': [],
'achievements': [],
'performance_data': []
})
# 計算排名
total_employees = EmployeePoint.query.count()
better_employees = EmployeePoint.query.filter(EmployeePoint.total_points > user_points.total_points).count()
total_rank = better_employees + 1
# 模擬最近活動
recent_activities = [
{
'type': 'assessment',
'title': '完成能力評估',
'description': '對溝通能力進行了評估',
'points': 20,
'created_at': '2024-01-15T10:30:00Z'
},
{
'type': 'feedback',
'title': '收到STAR回饋',
'description': '來自同事的正面回饋',
'points': 15,
'created_at': '2024-01-14T14:20:00Z'
}
]
# 模擬成就
achievements = [
{
'name': '評估達人',
'description': '完成10次能力評估',
'icon': 'clipboard-check'
},
{
'name': '回饋專家',
'description': '提供5次STAR回饋',
'icon': 'star-fill'
}
]
# 模擬績效數據
performance_data = [
{'month': '1月', 'points': 50},
{'month': '2月', 'points': 75},
{'month': '3月', 'points': 60},
{'month': '4月', 'points': 90},
{'month': '5月', 'points': 80}
]
return jsonify({
'points_summary': {
'total_points': user_points.total_points,
'monthly_points': user_points.monthly_points,
'department_rank': 1,
'total_rank': total_rank
},
'recent_activities': recent_activities,
'achievements': achievements,
'performance_data': performance_data
})
@app.route('/api/rankings/total', methods=['GET'])
def get_total_rankings():
department = request.args.get('department')
limit = request.args.get('limit', 50, type=int)
# 構建查詢
query = EmployeePoint.query
if department:
query = query.filter(EmployeePoint.department == department)
employees = query.order_by(EmployeePoint.total_points.desc()).limit(limit).all()
total_count = query.count()
rankings = []
for rank, employee in enumerate(employees, 1):
# 計算百分位數
percentile = ((total_count - rank + 1) / total_count * 100) if total_count > 0 else 0
rankings.append({
'rank': rank,
'employee_name': employee.employee_name,
'department': employee.department,
'position': employee.position,
'total_points': employee.total_points,
'monthly_points': employee.monthly_points,
'percentile': round(percentile, 1),
'tier': get_tier_by_percentile(percentile)
})
return jsonify({
'rankings': rankings,
'total_count': total_count,
'department_filter': department
})
@app.route('/api/rankings/advanced', methods=['GET'])
def get_advanced_rankings():
"""高級排名系統,包含更多統計信息"""
department = request.args.get('department')
position = request.args.get('position')
min_points = request.args.get('min_points', 0, type=int)
max_points = request.args.get('max_points', type=int)
# 構建查詢
query = EmployeePoint.query
if department:
query = query.filter(EmployeePoint.department == department)
if position:
query = query.filter(EmployeePoint.position.like(f'%{position}%'))
if min_points:
query = query.filter(EmployeePoint.total_points >= min_points)
if max_points:
query = query.filter(EmployeePoint.total_points <= max_points)
employees = query.order_by(EmployeePoint.total_points.desc()).all()
total_count = len(employees)
if total_count == 0:
return jsonify({
'rankings': [],
'statistics': {},
'filters': {
'department': department,
'position': position,
'min_points': min_points,
'max_points': max_points
}
})
# 計算統計信息
points_list = [emp.total_points for emp in employees]
avg_points = sum(points_list) / len(points_list)
median_points = sorted(points_list)[len(points_list) // 2]
max_points_val = max(points_list)
min_points_val = min(points_list)
rankings = []
for rank, employee in enumerate(employees, 1):
percentile = ((total_count - rank + 1) / total_count * 100) if total_count > 0 else 0
rankings.append({
'rank': rank,
'employee_name': employee.employee_name,
'department': employee.department,
'position': employee.position,
'total_points': employee.total_points,
'monthly_points': employee.monthly_points,
'percentile': round(percentile, 1),
'tier': get_tier_by_percentile(percentile),
'vs_average': round(employee.total_points - avg_points, 1),
'vs_median': round(employee.total_points - median_points, 1)
})
return jsonify({
'rankings': rankings,
'statistics': {
'total_count': total_count,
'average_points': round(avg_points, 1),
'median_points': median_points,
'max_points': max_points_val,
'min_points': min_points_val,
'standard_deviation': round(calculate_standard_deviation(points_list), 1)
},
'filters': {
'department': department,
'position': position,
'min_points': min_points,
'max_points': max_points
}
})
def get_tier_by_percentile(percentile):
"""根據百分位數返回等級"""
if percentile >= 95:
return {'name': '大師', 'color': 'danger', 'icon': 'crown'}
elif percentile >= 85:
return {'name': '專家', 'color': 'warning', 'icon': 'star-fill'}
elif percentile >= 70:
return {'name': '熟練', 'color': 'info', 'icon': 'award'}
elif percentile >= 50:
return {'name': '良好', 'color': 'success', 'icon': 'check-circle'}
else:
return {'name': '基礎', 'color': 'secondary', 'icon': 'circle'}
def calculate_standard_deviation(data):
"""計算標準差"""
if len(data) <= 1:
return 0
mean = sum(data) / len(data)
variance = sum((x - mean) ** 2 for x in data) / (len(data) - 1)
return variance ** 0.5
@app.route('/api/notifications', methods=['GET'])
def get_notifications():
"""獲取用戶通知"""
# 模擬通知數據
notifications = [
{
'id': 1,
'type': 'achievement',
'title': '🎉 恭喜獲得新成就!',
'message': '您已完成10次能力評估獲得「評估達人」徽章',
'is_read': False,
'created_at': '2024-01-15T10:30:00Z',
'icon': 'trophy-fill',
'color': 'warning'
},
{
'id': 2,
'type': 'ranking',
'title': '📈 排名更新',
'message': '您的總排名上升了3位目前排名第5名',
'is_read': False,
'created_at': '2024-01-14T15:20:00Z',
'icon': 'arrow-up-circle',
'color': 'success'
},
{
'id': 3,
'type': 'feedback',
'title': '⭐ 收到新回饋',
'message': '同事張三為您提供了STAR回饋請查看詳情',
'is_read': True,
'created_at': '2024-01-13T09:15:00Z',
'icon': 'star-fill',
'color': 'info'
},
{
'id': 4,
'type': 'system',
'title': '🔔 系統通知',
'message': '新的評估項目已上線,歡迎體驗新功能',
'is_read': True,
'created_at': '2024-01-12T14:00:00Z',
'icon': 'bell-fill',
'color': 'primary'
}
]
return jsonify({
'notifications': notifications,
'unread_count': len([n for n in notifications if not n['is_read']])
})
@app.route('/api/notifications/<int:notification_id>/read', methods=['POST'])
def mark_notification_read(notification_id):
"""標記通知為已讀"""
# 在實際應用中,這裡會更新數據庫
return jsonify({'success': True, 'message': '通知已標記為已讀'})
@app.route('/api/notifications/read-all', methods=['POST'])
def mark_all_notifications_read():
"""標記所有通知為已讀"""
# 在實際應用中,這裡會更新數據庫
return jsonify({'success': True, 'message': '所有通知已標記為已讀'})
@app.route('/api/admin/users', methods=['GET'])
def get_admin_users():
"""獲取所有用戶(管理員功能)"""
users = User.query.all()
users_data = []
for user in users:
users_data.append({
'id': user.id,
'username': user.username,
'email': user.email,
'full_name': user.full_name,
'department': user.department,
'position': user.position,
'employee_id': user.employee_id,
'is_active': user.is_active,
'created_at': user.created_at.isoformat() if user.created_at else None
})
return jsonify({'users': users_data})
@app.route('/api/admin/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
"""更新用戶信息(管理員功能)"""
user = User.query.get_or_404(user_id)
data = request.get_json()
if 'is_active' in data:
user.is_active = data['is_active']
if 'department' in data:
user.department = data['department']
if 'position' in data:
user.position = data['position']
db.session.commit()
return jsonify({'success': True, 'message': '用戶信息已更新'})
@app.route('/api/admin/statistics', methods=['GET'])
def get_admin_statistics():
"""獲取管理員統計信息"""
total_users = User.query.count()
active_users = User.query.filter_by(is_active=True).count()
total_assessments = Assessment.query.count()
total_feedbacks = StarFeedback.query.count()
# 部門統計
department_stats = db.session.query(
User.department,
db.func.count(User.id).label('count')
).group_by(User.department).all()
# 積分統計
points_stats = db.session.query(
db.func.avg(EmployeePoint.total_points).label('avg_points'),
db.func.max(EmployeePoint.total_points).label('max_points'),
db.func.min(EmployeePoint.total_points).label('min_points')
).first()
return jsonify({
'total_users': total_users,
'active_users': active_users,
'total_assessments': total_assessments,
'total_feedbacks': total_feedbacks,
'department_stats': [{'department': d[0], 'count': d[1]} for d in department_stats],
'points_stats': {
'average': round(points_stats.avg_points or 0, 1),
'maximum': points_stats.max_points or 0,
'minimum': points_stats.min_points or 0
}
})
def create_sample_data():
"""創建樣本數據"""
# 創建能力項目
capabilities = [
Capability(
name='溝通能力',
l1_description='基本溝通',
l2_description='有效溝通',
l3_description='專業溝通',
l4_description='領導溝通',
l5_description='戰略溝通'
),
Capability(
name='技術能力',
l1_description='基礎技術',
l2_description='熟練技術',
l3_description='專業技術',
l4_description='專家技術',
l5_description='大師技術'
),
Capability(
name='領導能力',
l1_description='自我管理',
l2_description='團隊協作',
l3_description='團隊領導',
l4_description='部門領導',
l5_description='戰略領導'
)
]
for cap in capabilities:
existing = Capability.query.filter_by(name=cap.name).first()
if not existing:
db.session.add(cap)
# 創建測試帳號
test_accounts = [
{
'username': 'admin',
'email': 'admin@company.com',
'full_name': '系統管理員',
'department': 'IT',
'position': '系統管理員',
'password_hash': 'admin123' # 簡化版直接存儲密碼
},
{
'username': 'hr_manager',
'email': 'hr@company.com',
'full_name': 'HR主管',
'department': 'HR',
'position': '人力資源主管',
'password_hash': 'hr123'
},
{
'username': 'user',
'email': 'user@company.com',
'full_name': '一般用戶',
'department': 'IT',
'position': '軟體工程師',
'password_hash': 'user123'
}
]
for account in test_accounts:
existing_user = User.query.filter_by(username=account['username']).first()
if not existing_user:
user = User(**account)
db.session.add(user)
print(f"創建測試帳號: {account['username']}")
# 創建樣本積分數據
sample_points_data = [
{
'employee_name': '系統管理員',
'department': 'IT',
'position': '系統管理員',
'total_points': 150,
'monthly_points': 50
},
{
'employee_name': 'HR主管',
'department': 'HR',
'position': '人力資源主管',
'total_points': 120,
'monthly_points': 40
},
{
'employee_name': '一般用戶',
'department': 'IT',
'position': '軟體工程師',
'total_points': 80,
'monthly_points': 30
}
]
for points_data in sample_points_data:
existing = EmployeePoint.query.filter_by(employee_name=points_data['employee_name']).first()
if not existing:
points = EmployeePoint(**points_data)
db.session.add(points)
db.session.commit()
print("測試帳號和樣本數據創建完成")
if __name__ == '__main__':
with app.app_context():
# 創建所有數據庫表
db.create_all()
print("數據庫表創建成功!")
# 創建樣本數據
create_sample_data()
print("=" * 60)
print("夥伴對齊系統已啟動!")
print("=" * 60)
print("[WEB] 訪問地址: http://localhost:5000")
print()
print("[ACCOUNT] 測試帳號資訊:")
print(" 管理員: admin / admin123")
print(" HR主管: hr_manager / hr123")
print(" 一般用戶: user / user123")
print()
print("[FEATURES] 功能包括:")
print("- 能力評估系統")
print("- STAR 回饋系統")
print("- 排名系統")
print("- 數據導出")
print()
print("[TIP] 提示: 登入頁面會顯示測試帳號資訊和快速登入按鈕")
print("=" * 60)
# 啟動應用程式
app.run(debug=True, host='0.0.0.0', port=5000)

507
static/css/style.css Normal file
View File

@@ -0,0 +1,507 @@
/* 夥伴對齊系統 - 樣式表 */
:root {
--primary-color: #0d6efd;
--secondary-color: #6c757d;
--success-color: #198754;
--danger-color: #dc3545;
--warning-color: #ffc107;
--info-color: #0dcaf0;
--light-color: #f8f9fa;
--dark-color: #212529;
--border-radius: 0.375rem;
--box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--transition: all 0.15s ease-in-out;
}
/* 全局樣式 */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
line-height: 1.6;
}
/* 導航欄樣式 */
.navbar-brand {
font-weight: 600;
font-size: 1.25rem;
}
.navbar-nav .nav-link {
font-weight: 500;
transition: var(--transition);
border-radius: var(--border-radius);
margin: 0 0.25rem;
}
.navbar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
transform: translateY(-1px);
}
.navbar-nav .nav-link.active {
background-color: rgba(255, 255, 255, 0.2);
font-weight: 600;
}
/* 卡片樣式 */
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
transition: var(--transition);
margin-bottom: 1.5rem;
}
.card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.card-header {
background-color: #fff;
border-bottom: 1px solid #dee2e6;
font-weight: 600;
padding: 1rem 1.25rem;
}
.card-body {
padding: 1.25rem;
}
/* 儀表板卡片樣式 */
.card.bg-primary,
.card.bg-success,
.card.bg-warning,
.card.bg-info {
border: none;
background: linear-gradient(135deg, var(--primary-color), #0056b3);
}
.card.bg-success {
background: linear-gradient(135deg, var(--success-color), #146c43);
}
.card.bg-warning {
background: linear-gradient(135deg, var(--warning-color), #e0a800);
}
.card.bg-info {
background: linear-gradient(135deg, var(--info-color), #0aa2c0);
}
/* 按鈕樣式 */
.btn {
border-radius: var(--border-radius);
font-weight: 500;
transition: var(--transition);
border: none;
padding: 0.5rem 1rem;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.15);
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), #0056b3);
}
.btn-success {
background: linear-gradient(135deg, var(--success-color), #146c43);
}
.btn-danger {
background: linear-gradient(135deg, var(--danger-color), #b02a37);
}
.btn-warning {
background: linear-gradient(135deg, var(--warning-color), #e0a800);
color: #000;
}
.btn-info {
background: linear-gradient(135deg, var(--info-color), #0aa2c0);
}
/* 表單樣式 */
.form-control,
.form-select {
border-radius: var(--border-radius);
border: 1px solid #ced4da;
transition: var(--transition);
padding: 0.5rem 0.75rem;
}
.form-control:focus,
.form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
.form-label {
font-weight: 500;
color: var(--dark-color);
margin-bottom: 0.5rem;
}
/* 能力評估樣式 */
.capability-card {
border-left: 4px solid var(--primary-color);
transition: var(--transition);
}
.capability-card:hover {
border-left-color: var(--success-color);
background-color: #f8f9fa;
}
.form-check-input:checked {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.form-check-label {
font-size: 0.9rem;
line-height: 1.4;
}
/* 排名表格樣式 */
.table {
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--box-shadow);
}
.table thead th {
background-color: var(--primary-color);
color: white;
border: none;
font-weight: 600;
padding: 1rem 0.75rem;
}
.table tbody tr {
transition: var(--transition);
}
.table tbody tr:hover {
background-color: #f8f9fa;
transform: scale(1.01);
}
/* 徽章樣式 */
.badge {
font-size: 0.75rem;
font-weight: 500;
padding: 0.375rem 0.75rem;
border-radius: var(--border-radius);
}
/* 通知樣式 */
.alert {
border: none;
border-radius: var(--border-radius);
border-left: 4px solid;
margin-bottom: 1rem;
}
.alert-primary {
border-left-color: var(--primary-color);
background-color: rgba(13, 110, 253, 0.1);
}
.alert-success {
border-left-color: var(--success-color);
background-color: rgba(25, 135, 84, 0.1);
}
.alert-warning {
border-left-color: var(--warning-color);
background-color: rgba(255, 193, 7, 0.1);
}
.alert-danger {
border-left-color: var(--danger-color);
background-color: rgba(220, 53, 69, 0.1);
}
/* 模態框樣式 */
.modal-content {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175);
}
.modal-header {
border-bottom: 1px solid #dee2e6;
background-color: #f8f9fa;
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
.modal-footer {
border-top: 1px solid #dee2e6;
background-color: #f8f9fa;
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
/* 載入動畫 */
.spinner-border {
width: 3rem;
height: 3rem;
}
/* Toast 樣式 */
.toast {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.toast-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
/* 標籤頁樣式 */
.nav-tabs .nav-link {
border: none;
border-radius: var(--border-radius) var(--border-radius) 0 0;
color: var(--secondary-color);
font-weight: 500;
transition: var(--transition);
}
.nav-tabs .nav-link:hover {
border-color: transparent;
background-color: #f8f9fa;
}
.nav-tabs .nav-link.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
/* 下拉選單樣式 */
.dropdown-menu {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
padding: 0.5rem 0;
}
.dropdown-item {
padding: 0.5rem 1rem;
transition: var(--transition);
}
.dropdown-item:hover {
background-color: #f8f9fa;
color: var(--primary-color);
}
/* 響應式設計 */
@media (max-width: 768px) {
.container-fluid {
padding: 0.5rem;
}
.card {
margin-bottom: 1rem;
}
.card-body {
padding: 1rem;
}
.table-responsive {
font-size: 0.875rem;
}
.btn {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
}
@media (max-width: 576px) {
.navbar-brand {
font-size: 1rem;
}
.card-header h5 {
font-size: 1rem;
}
.modal-dialog {
margin: 0.5rem;
}
}
/* 動畫效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section {
animation: fadeIn 0.5s ease-in-out;
}
/* 自定義滾動條 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #0056b3;
}
/* 工具提示樣式 */
.tooltip {
font-size: 0.875rem;
}
.tooltip-inner {
background-color: var(--dark-color);
border-radius: var(--border-radius);
}
/* 進度條樣式 */
.progress {
height: 0.5rem;
border-radius: var(--border-radius);
background-color: #e9ecef;
}
.progress-bar {
border-radius: var(--border-radius);
transition: width 0.6s ease;
}
/* 統計卡片特殊樣式 */
.stats-card {
position: relative;
overflow: hidden;
}
.stats-card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
transform: translate(30px, -30px);
}
.stats-card .card-body {
position: relative;
z-index: 1;
}
/* 排名特殊樣式 */
.ranking-item {
transition: var(--transition);
border-radius: var(--border-radius);
padding: 0.75rem;
margin-bottom: 0.5rem;
background: white;
border-left: 4px solid var(--primary-color);
}
.ranking-item:hover {
transform: translateX(5px);
box-shadow: var(--box-shadow);
}
.ranking-item.top-3 {
border-left-color: var(--warning-color);
background: linear-gradient(135deg, #fff9e6, #ffffff);
}
.ranking-item.top-1 {
border-left-color: #ffd700;
background: linear-gradient(135deg, #fffacd, #ffffff);
}
/* 能力等級標籤 */
.level-badge {
font-size: 0.75rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.level-l1 { background-color: var(--danger-color); color: white; }
.level-l2 { background-color: var(--warning-color); color: #000; }
.level-l3 { background-color: var(--info-color); color: white; }
.level-l4 { background-color: var(--success-color); color: white; }
.level-l5 { background-color: var(--primary-color); color: white; }
/* 空狀態樣式 */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--secondary-color);
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
/* 載入狀態 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
border-radius: var(--border-radius);
}
/* 成功/錯誤狀態 */
.status-success {
color: var(--success-color);
}
.status-error {
color: var(--danger-color);
}
.status-warning {
color: var(--warning-color);
}
.status-info {
color: var(--info-color);
}

324
static/js/admin.js Normal file
View File

@@ -0,0 +1,324 @@
// Admin functionality JavaScript
class AdminManager {
constructor() {
this.initializeEventListeners();
}
initializeEventListeners() {
// Export buttons
document.addEventListener('click', (e) => {
if (e.target.matches('[onclick*="exportData"]')) {
e.preventDefault();
const onclick = e.target.getAttribute('onclick');
const matches = onclick.match(/exportData\('([^']+)',\s*'([^']+)'\)/);
if (matches) {
this.exportData(matches[1], matches[2]);
}
}
});
// Calculate rankings button
document.addEventListener('click', (e) => {
if (e.target.matches('[onclick*="calculateMonthlyRankings"]')) {
e.preventDefault();
this.calculateMonthlyRankings();
}
});
}
async exportData(type, format) {
try {
this.showLoading(true, `正在匯出${type === 'assessments' ? '評估資料' : 'STAR回饋'}...`);
const url = `/api/export/${type}?format=${format}`;
// Create a temporary link to download the file
const link = document.createElement('a');
link.href = url;
link.download = `${type}_${new Date().toISOString().split('T')[0]}.${format === 'excel' ? 'xlsx' : 'csv'}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.showSuccess(`${type === 'assessments' ? '評估資料' : 'STAR回饋'}匯出成功`);
} catch (error) {
console.error('Export failed:', error);
this.showError('匯出失敗: ' + error.message);
} finally {
this.showLoading(false);
}
}
async calculateMonthlyRankings() {
try {
const year = document.getElementById('calc-year').value;
const month = document.getElementById('calc-month').value;
if (!year || !month) {
this.showError('請選擇年份和月份');
return;
}
this.showLoading(true, '正在計算月度排名...');
const response = await fetch('/api/rankings/calculate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
year: parseInt(year),
month: parseInt(month)
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
this.showSuccess(result.message);
} catch (error) {
console.error('Calculate rankings failed:', error);
this.showError('排名計算失敗: ' + error.message);
} finally {
this.showLoading(false);
}
}
showLoading(show, message = '處理中...') {
// You can implement a more sophisticated loading indicator here
if (show) {
console.log('Loading:', message);
} else {
console.log('Loading finished');
}
}
showSuccess(message) {
console.log('Success:', message);
if (window.showSuccess) {
window.showSuccess(message);
}
}
showError(message) {
console.error('Error:', message);
if (window.showError) {
window.showError(message);
}
}
}
// Data management utilities
class DataManager {
constructor() {
this.initializeDataManagement();
}
initializeDataManagement() {
// Add any data management functionality here
this.setupDataValidation();
}
setupDataValidation() {
// Validate data integrity
this.validateDataIntegrity();
}
async validateDataIntegrity() {
try {
// Check for orphaned records, missing references, etc.
console.log('Validating data integrity...');
// You can add specific validation logic here
} catch (error) {
console.error('Data validation failed:', error);
}
}
async getSystemStats() {
try {
const [assessments, feedbacks, employees] = await Promise.all([
fetch('/api/assessments?per_page=1').then(r => r.json()),
fetch('/api/star-feedbacks?per_page=1').then(r => r.json()),
fetch('/api/rankings/total?limit=1').then(r => r.json())
]);
return {
totalAssessments: assessments.total || 0,
totalFeedbacks: feedbacks.total || 0,
totalEmployees: employees.rankings.length || 0
};
} catch (error) {
console.error('Failed to get system stats:', error);
return null;
}
}
}
// Export functionality
class ExportManager {
constructor() {
this.supportedFormats = ['excel', 'csv'];
}
async exportAssessments(format = 'excel') {
return this.exportData('assessments', format);
}
async exportStarFeedbacks(format = 'excel') {
return this.exportData('star-feedbacks', format);
}
async exportData(type, format) {
if (!this.supportedFormats.includes(format)) {
throw new Error(`Unsupported format: ${format}`);
}
const url = `/api/export/${type}?format=${format}`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Export failed: ${response.status}`);
}
// Get filename from response headers or create default
const contentDisposition = response.headers.get('Content-Disposition');
let filename = `${type}_${new Date().toISOString().split('T')[0]}.${format === 'excel' ? 'xlsx' : 'csv'}`;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// Create blob and download
const blob = await response.blob();
this.downloadBlob(blob, filename);
return filename;
} catch (error) {
console.error('Export failed:', error);
throw error;
}
}
downloadBlob(blob, filename) {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
}
// Ranking calculation utilities
class RankingCalculator {
constructor() {
this.initializeRankingCalculation();
}
initializeRankingCalculation() {
// Add any ranking calculation setup here
}
async calculateMonthlyRankings(year, month) {
try {
const response = await fetch('/api/rankings/calculate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ year, month })
});
if (!response.ok) {
throw new Error(`Calculation failed: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('Ranking calculation failed:', error);
throw error;
}
}
async getRankingHistory(year, month) {
try {
const response = await fetch(`/api/rankings/monthly?year=${year}&month=${month}`);
if (!response.ok) {
throw new Error(`Failed to get ranking history: ${response.status}`);
}
const result = await response.json();
return result;
} catch (error) {
console.error('Failed to get ranking history:', error);
throw error;
}
}
}
// Initialize admin functionality when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Initialize admin manager
window.adminManager = new AdminManager();
// Initialize data manager
window.dataManager = new DataManager();
// Initialize export manager
window.exportManager = new ExportManager();
// Initialize ranking calculator
window.rankingCalculator = new RankingCalculator();
});
// Global functions for HTML onclick handlers
function exportData(type, format) {
if (window.exportManager) {
window.exportManager.exportData(type, format);
}
}
function calculateMonthlyRankings() {
if (window.rankingCalculator) {
const year = document.getElementById('calc-year').value;
const month = document.getElementById('calc-month').value;
if (!year || !month) {
if (window.showError) {
window.showError('請選擇年份和月份');
}
return;
}
window.rankingCalculator.calculateMonthlyRankings(parseInt(year), parseInt(month))
.then(result => {
if (window.showSuccess) {
window.showSuccess(result.message);
}
})
.catch(error => {
if (window.showError) {
window.showError('排名計算失敗: ' + error.message);
}
});
}
}

1646
static/js/app.js Normal file

File diff suppressed because it is too large Load Diff

386
static/js/assessment.js Normal file
View File

@@ -0,0 +1,386 @@
// Assessment-specific JavaScript functionality
// Enhanced drag and drop for assessment
class AssessmentDragDrop {
constructor() {
this.draggedElement = null;
this.initializeDragDrop();
}
initializeDragDrop() {
// Set up drag and drop for capability items
document.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('capability-item')) {
this.handleDragStart(e);
}
});
document.addEventListener('dragover', (e) => {
if (e.target.closest('.drop-zone')) {
this.handleDragOver(e);
}
});
document.addEventListener('dragleave', (e) => {
if (e.target.closest('.drop-zone')) {
this.handleDragLeave(e);
}
});
document.addEventListener('drop', (e) => {
if (e.target.closest('.drop-zone')) {
this.handleDrop(e);
}
});
document.addEventListener('dragend', (e) => {
if (e.target.classList.contains('capability-item')) {
this.handleDragEnd(e);
}
});
}
handleDragStart(e) {
this.draggedElement = e.target;
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.outerHTML);
e.dataTransfer.setData('text/plain', e.target.dataset.capabilityId);
}
handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const dropZone = e.target.closest('.drop-zone');
if (dropZone) {
dropZone.classList.add('drag-over');
}
}
handleDragLeave(e) {
const dropZone = e.target.closest('.drop-zone');
if (dropZone && !dropZone.contains(e.relatedTarget)) {
dropZone.classList.remove('drag-over');
}
}
handleDrop(e) {
e.preventDefault();
const dropZone = e.target.closest('.drop-zone');
if (dropZone) {
dropZone.classList.remove('drag-over');
if (this.draggedElement) {
this.moveCapability(this.draggedElement, dropZone);
}
}
}
handleDragEnd(e) {
e.target.classList.remove('dragging');
this.draggedElement = null;
}
moveCapability(capabilityElement, targetDropZone) {
// Remove from original position
capabilityElement.remove();
// Create new element in target drop zone
const newElement = this.createCapabilityElement(capabilityElement);
// Clear target drop zone and add new element
targetDropZone.innerHTML = '';
targetDropZone.appendChild(newElement);
targetDropZone.classList.add('has-items');
// Update visual state
this.updateDropZoneState(targetDropZone);
}
createCapabilityElement(originalElement) {
const newElement = document.createElement('div');
newElement.className = 'capability-item';
newElement.draggable = true;
newElement.dataset.capabilityId = originalElement.dataset.capabilityId;
newElement.dataset.capabilityName = originalElement.dataset.capabilityName;
newElement.textContent = originalElement.dataset.capabilityName;
// Add click to remove functionality
newElement.addEventListener('dblclick', () => {
this.removeCapabilityFromLevel(newElement);
});
return newElement;
}
removeCapabilityFromLevel(capabilityElement) {
const dropZone = capabilityElement.closest('.drop-zone');
if (dropZone) {
// Return to available capabilities
this.returnToAvailable(capabilityElement);
// Update drop zone state
this.updateDropZoneState(dropZone);
}
}
returnToAvailable(capabilityElement) {
const availableContainer = document.getElementById('available-capabilities');
if (availableContainer) {
const newElement = this.createCapabilityElement(capabilityElement);
availableContainer.appendChild(newElement);
}
}
updateDropZoneState(dropZone) {
const hasItems = dropZone.querySelector('.capability-item');
if (hasItems) {
dropZone.classList.add('has-items');
dropZone.classList.remove('empty');
} else {
dropZone.classList.remove('has-items');
dropZone.classList.add('empty');
dropZone.innerHTML = '<div class="drop-placeholder">拖放能力到此處</div>';
}
}
}
// Capability management
class CapabilityManager {
constructor() {
this.capabilities = [];
this.loadCapabilities();
}
async loadCapabilities() {
try {
const response = await fetch('/api/capabilities');
const data = await response.json();
this.capabilities = data.capabilities;
this.displayCapabilities();
} catch (error) {
console.error('Failed to load capabilities:', error);
this.showError('載入能力清單失敗');
}
}
displayCapabilities() {
const container = document.getElementById('available-capabilities');
if (!container) return;
container.innerHTML = '';
this.capabilities.forEach(capability => {
const capabilityElement = this.createCapabilityElement(capability);
container.appendChild(capabilityElement);
});
}
createCapabilityElement(capability) {
const element = document.createElement('div');
element.className = 'capability-item';
element.draggable = true;
element.dataset.capabilityId = capability.id;
element.dataset.capabilityName = capability.name;
element.textContent = capability.name;
// Add tooltip with description
element.title = this.getCapabilityDescription(capability);
return element;
}
getCapabilityDescription(capability) {
const descriptions = [
capability.l1_description,
capability.l2_description,
capability.l3_description,
capability.l4_description,
capability.l5_description
].filter(desc => desc && desc.trim());
return descriptions.join('\n\n');
}
showError(message) {
// You can implement a more sophisticated error display here
console.error(message);
}
}
// Assessment form validation and submission
class AssessmentForm {
constructor() {
this.form = document.getElementById('assessment-form');
this.initializeForm();
}
initializeForm() {
if (this.form) {
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.handleSubmit();
});
}
}
async handleSubmit() {
if (!this.validateForm()) {
return;
}
const assessmentData = this.collectAssessmentData();
try {
this.showLoading(true);
const response = await fetch('/api/assessments', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(assessmentData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
this.showSuccess(result.message);
this.clearForm();
} catch (error) {
console.error('Assessment submission failed:', error);
this.showError('評估提交失敗: ' + error.message);
} finally {
this.showLoading(false);
}
}
validateForm() {
const department = document.getElementById('department').value.trim();
const position = document.getElementById('position').value.trim();
if (!department) {
this.showError('請填寫部門');
return false;
}
if (!position) {
this.showError('請填寫職位');
return false;
}
// Check if at least one capability is assigned
const hasAssignedCapabilities = this.hasAssignedCapabilities();
if (!hasAssignedCapabilities) {
this.showError('請至少分配一個能力到某個等級');
return false;
}
return true;
}
hasAssignedCapabilities() {
const levels = ['L1', 'L2', 'L3', 'L4', 'L5'];
for (let level of levels) {
const dropZone = document.getElementById(`level-${level.toLowerCase()}`);
if (dropZone && dropZone.querySelector('.capability-item')) {
return true;
}
}
return false;
}
collectAssessmentData() {
const formData = new FormData(this.form);
const assessmentData = {
department: formData.get('department'),
position: formData.get('position'),
employee_name: formData.get('employee_name') || null,
assessment_data: {}
};
// Collect capability assignments
const levels = ['L1', 'L2', 'L3', 'L4', 'L5'];
levels.forEach(level => {
const dropZone = document.getElementById(`level-${level.toLowerCase()}`);
const capabilityItems = dropZone.querySelectorAll('.capability-item');
assessmentData.assessment_data[level] = Array.from(capabilityItems).map(item => item.dataset.capabilityName);
});
return assessmentData;
}
clearForm() {
this.form.reset();
// Clear all drop zones
const levels = ['L1', 'L2', 'L3', 'L4', 'L5'];
levels.forEach(level => {
const dropZone = document.getElementById(`level-${level.toLowerCase()}`);
if (dropZone) {
dropZone.innerHTML = '<div class="drop-placeholder">拖放能力到此處</div>';
dropZone.classList.remove('has-items');
}
});
// Reload capabilities
if (window.capabilityManager) {
window.capabilityManager.loadCapabilities();
}
}
showLoading(show) {
const submitButton = this.form.querySelector('button[type="submit"]');
if (submitButton) {
if (show) {
submitButton.disabled = true;
submitButton.innerHTML = '<span class="loading"></span> 儲存中...';
} else {
submitButton.disabled = false;
submitButton.innerHTML = '<i class="bi bi-save me-1"></i>儲存評估';
}
}
}
showSuccess(message) {
// You can implement a more sophisticated success display here
console.log('Success:', message);
if (window.showSuccess) {
window.showSuccess(message);
}
}
showError(message) {
// You can implement a more sophisticated error display here
console.error('Error:', message);
if (window.showError) {
window.showError(message);
}
}
}
// Initialize assessment functionality when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Initialize drag and drop
window.assessmentDragDrop = new AssessmentDragDrop();
// Initialize capability manager
window.capabilityManager = new CapabilityManager();
// Initialize assessment form
window.assessmentForm = new AssessmentForm();
});
// Global function for clearing assessment (called from HTML)
function clearAssessment() {
if (window.assessmentForm) {
window.assessmentForm.clearForm();
}
}

925
templates/index.html Normal file
View File

@@ -0,0 +1,925 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>夥伴對齊系統 - Partner Alignment System</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
</head>
<body>
<!-- Login Modal -->
<div class="modal fade" id="loginModal" tabindex="-1" aria-labelledby="loginModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="loginModalLabel">
<i class="bi bi-person-circle me-2"></i>登入系統
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="loginForm">
<div class="mb-3">
<label for="loginUsername" class="form-label">用戶名</label>
<input type="text" class="form-control" id="loginUsername" required>
</div>
<div class="mb-3">
<label for="loginPassword" class="form-label">密碼</label>
<input type="password" class="form-control" id="loginPassword" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-box-arrow-in-right me-2"></i>登入
</button>
</div>
</form>
<!-- Test Accounts Information -->
<div class="card mt-3">
<div class="card-header bg-light">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>測試帳號資訊
</h6>
</div>
<div class="card-body p-3">
<div class="row">
<div class="col-12">
<div class="mb-2">
<strong class="text-success">👑 管理員</strong><br>
<small class="text-muted">用戶名: admin | 密碼: admin123</small>
</div>
<div class="mb-2">
<strong class="text-info">👥 HR主管</strong><br>
<small class="text-muted">用戶名: hr_manager | 密碼: hr123</small>
</div>
<div class="mb-2">
<strong class="text-primary">👤 一般用戶</strong><br>
<small class="text-muted">用戶名: user | 密碼: user123</small>
</div>
</div>
</div>
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="fillLoginForm('admin', 'admin123')">
<i class="bi bi-person-gear me-1"></i>管理員登入
</button>
<button type="button" class="btn btn-sm btn-outline-info" onclick="fillLoginForm('hr_manager', 'hr123')">
<i class="bi bi-people me-1"></i>HR主管登入
</button>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="fillLoginForm('user', 'user123')">
<i class="bi bi-person me-1"></i>一般用戶登入
</button>
</div>
</div>
</div>
<div class="text-center mt-3">
<small class="text-muted">
沒有帳號?<a href="#" id="showRegisterModal">立即註冊</a>
</small>
</div>
</div>
</div>
</div>
</div>
<!-- Register Modal -->
<div class="modal fade" id="registerModal" tabindex="-1" aria-labelledby="registerModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="registerModalLabel">
<i class="bi bi-person-plus me-2"></i>註冊新用戶
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="registerForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="regUsername" class="form-label">用戶名 *</label>
<input type="text" class="form-control" id="regUsername" required>
</div>
<div class="mb-3">
<label for="regEmail" class="form-label">電子郵件 *</label>
<input type="email" class="form-control" id="regEmail" required>
</div>
<div class="mb-3">
<label for="regPassword" class="form-label">密碼 *</label>
<input type="password" class="form-control" id="regPassword" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="regFullName" class="form-label">姓名 *</label>
<input type="text" class="form-control" id="regFullName" required>
</div>
<div class="mb-3">
<label for="regDepartment" class="form-label">部門 *</label>
<select class="form-select" id="regDepartment" required>
<option value="">請選擇部門</option>
<option value="IT">IT部門</option>
<option value="HR">人力資源部</option>
<option value="Finance">財務部</option>
<option value="Marketing">行銷部</option>
<option value="Sales">業務部</option>
<option value="Operations">營運部</option>
</select>
</div>
<div class="mb-3">
<label for="regPosition" class="form-label">職位 *</label>
<input type="text" class="form-control" id="regPosition" required>
</div>
<div class="mb-3">
<label for="regEmployeeId" class="form-label">員工編號 *</label>
<input type="text" class="form-control" id="regEmployeeId" required>
</div>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success">
<i class="bi bi-person-check me-2"></i>註冊
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="#">
<i class="bi bi-people-fill me-2"></i>夥伴對齊系統
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto" id="mainNav">
<li class="nav-item">
<a class="nav-link" href="#dashboard" data-section="dashboard">
<i class="bi bi-speedometer2 me-1"></i>個人儀表板
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#assessment" data-section="assessment">
<i class="bi bi-clipboard-check me-1"></i>能力評估
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#star-feedback" data-section="star-feedback">
<i class="bi bi-star-fill me-1"></i>STAR回饋
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#rankings" data-section="rankings">
<i class="bi bi-trophy-fill me-1"></i>排名系統
</a>
</li>
<li class="nav-item" id="adminNavItem" style="display: none;">
<a class="nav-link" href="#admin" data-section="admin">
<i class="bi bi-gear-fill me-1"></i>系統管理
</a>
</li>
</ul>
<ul class="navbar-nav" id="authNav">
<li class="nav-item">
<button class="btn btn-outline-light" id="loginBtn">
<i class="bi bi-box-arrow-in-right me-1"></i>登入
</button>
</li>
</ul>
<ul class="navbar-nav" id="userNav" style="display: none;">
<!-- Notifications -->
<li class="nav-item dropdown me-3">
<a class="nav-link position-relative" href="#" role="button" data-bs-toggle="dropdown" id="notificationsDropdown">
<i class="bi bi-bell-fill"></i>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" id="notificationBadge" style="display: none;">
0
</span>
</a>
<ul class="dropdown-menu dropdown-menu-end" style="width: 350px;">
<li class="dropdown-header d-flex justify-content-between align-items-center">
<span>通知</span>
<button class="btn btn-sm btn-outline-secondary" id="markAllReadBtn">
<i class="bi bi-check-all me-1"></i>全部已讀
</button>
</li>
<li><hr class="dropdown-divider"></li>
<div id="notificationsList" style="max-height: 300px; overflow-y: auto;">
<div class="text-center text-muted py-3">
<i class="bi bi-bell" style="font-size: 2rem;"></i>
<p class="mt-2">暫無通知</p>
</div>
</div>
</ul>
</li>
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle me-1"></i><span id="userDisplayName">用戶</span>
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#profile" data-section="profile">
<i class="bi bi-person me-2"></i>個人資料
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" id="logoutBtn">
<i class="bi bi-box-arrow-right me-2"></i>登出
</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="container-fluid mt-4">
<!-- Login Required Message -->
<div id="loginRequired" class="text-center py-5">
<div class="card mx-auto" style="max-width: 400px;">
<div class="card-body">
<i class="bi bi-shield-lock text-primary" style="font-size: 3rem;"></i>
<h4 class="mt-3">請先登入</h4>
<p class="text-muted">您需要登入才能使用系統功能</p>
<button class="btn btn-primary" id="showLoginBtn">
<i class="bi bi-box-arrow-in-right me-2"></i>立即登入
</button>
</div>
</div>
</div>
<!-- Dashboard Section -->
<div id="dashboard-section" class="section" style="display: none;">
<div class="row">
<div class="col-12">
<h2 class="mb-4">
<i class="bi bi-speedometer2 me-2"></i>個人儀表板
</h2>
</div>
</div>
<div class="row" id="dashboardContent">
<!-- Points Summary -->
<div class="col-md-4 mb-4">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">總積分</h4>
<h2 id="totalPoints">0</h2>
</div>
<div class="align-self-center">
<i class="bi bi-trophy-fill" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Monthly Points -->
<div class="col-md-4 mb-4">
<div class="card bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">本月積分</h4>
<h2 id="monthlyPoints">0</h2>
</div>
<div class="align-self-center">
<i class="bi bi-calendar-month" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Current Rank -->
<div class="col-md-4 mb-4">
<div class="card bg-warning text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">當前排名</h4>
<h2 id="currentRank">#0</h2>
</div>
<div class="align-self-center">
<i class="bi bi-award" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Activities -->
<div class="col-md-8 mb-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-clock-history me-2"></i>最近活動
</h5>
</div>
<div class="card-body">
<div id="recentActivities">
<div class="text-center text-muted py-3">
<i class="bi bi-inbox" style="font-size: 2rem;"></i>
<p class="mt-2">暫無最近活動</p>
</div>
</div>
</div>
</div>
</div>
<!-- Achievements -->
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-patch-check me-2"></i>成就徽章
</h5>
</div>
<div class="card-body">
<div id="achievements">
<div class="text-center text-muted py-3">
<i class="bi bi-award" style="font-size: 2rem;"></i>
<p class="mt-2">暫無成就</p>
</div>
</div>
</div>
</div>
</div>
<!-- Performance Chart -->
<div class="col-12 mb-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-graph-up me-2"></i>積分趨勢
</h5>
</div>
<div class="card-body">
<canvas id="pointsChart" width="400" height="100"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Assessment Section -->
<div id="assessment-section" class="section" style="display: none;">
<div class="row">
<div class="col-12">
<h2 class="mb-4">
<i class="bi bi-clipboard-check me-2"></i>能力評估
</h2>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">能力評估表單</h5>
</div>
<div class="card-body">
<form id="assessmentForm">
<div class="row mb-3">
<div class="col-md-6">
<label for="assessmentDepartment" class="form-label">部門</label>
<select class="form-select" id="assessmentDepartment" required>
<option value="">請選擇部門</option>
<option value="IT">IT部門</option>
<option value="HR">人力資源部</option>
<option value="Finance">財務部</option>
<option value="Marketing">行銷部</option>
<option value="Sales">業務部</option>
<option value="Operations">營運部</option>
</select>
</div>
<div class="col-md-6">
<label for="assessmentPosition" class="form-label">職位</label>
<input type="text" class="form-control" id="assessmentPosition" required>
</div>
</div>
<div class="mb-3">
<label for="assessmentEmployeeName" class="form-label">員工姓名</label>
<input type="text" class="form-control" id="assessmentEmployeeName">
</div>
<div id="capabilitiesContainer">
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
請先選擇部門以載入對應的能力評估項目
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-2"></i>提交評估
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">評估說明</h5>
</div>
<div class="card-body">
<p class="text-muted">請根據以下標準進行評估:</p>
<ul class="list-unstyled">
<li><span class="badge bg-danger me-2">L1</span>初級</li>
<li><span class="badge bg-warning me-2">L2</span>中級</li>
<li><span class="badge bg-info me-2">L3</span>高級</li>
<li><span class="badge bg-success me-2">L4</span>專家</li>
<li><span class="badge bg-primary me-2">L5</span>大師</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- STAR Feedback Section -->
<div id="star-feedback-section" class="section" style="display: none;">
<div class="row">
<div class="col-12">
<h2 class="mb-4">
<i class="bi bi-star-fill me-2"></i>STAR回饋系統
</h2>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">STAR回饋表單</h5>
</div>
<div class="card-body">
<form id="starFeedbackForm">
<div class="row mb-3">
<div class="col-md-6">
<label for="evaluatorName" class="form-label">評估者姓名</label>
<input type="text" class="form-control" id="evaluatorName" required>
</div>
<div class="col-md-6">
<label for="evaluateeName" class="form-label">被評估者姓名</label>
<input type="text" class="form-control" id="evaluateeName" required>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="evaluateeDepartment" class="form-label">被評估者部門</label>
<select class="form-select" id="evaluateeDepartment" required>
<option value="">請選擇部門</option>
<option value="IT">IT部門</option>
<option value="HR">人力資源部</option>
<option value="Finance">財務部</option>
<option value="Marketing">行銷部</option>
<option value="Sales">業務部</option>
<option value="Operations">營運部</option>
</select>
</div>
<div class="col-md-6">
<label for="evaluateePosition" class="form-label">被評估者職位</label>
<input type="text" class="form-control" id="evaluateePosition" required>
</div>
</div>
<div class="mb-3">
<label for="situation" class="form-label">情況 (Situation)</label>
<textarea class="form-control" id="situation" rows="3" required></textarea>
</div>
<div class="mb-3">
<label for="task" class="form-label">任務 (Task)</label>
<textarea class="form-control" id="task" rows="3" required></textarea>
</div>
<div class="mb-3">
<label for="action" class="form-label">行動 (Action)</label>
<textarea class="form-control" id="action" rows="3" required></textarea>
</div>
<div class="mb-3">
<label for="result" class="form-label">結果 (Result)</label>
<textarea class="form-control" id="result" rows="3" required></textarea>
</div>
<div class="mb-3">
<label for="score" class="form-label">評分 (1-5分)</label>
<select class="form-select" id="score" required>
<option value="">請選擇評分</option>
<option value="1">1分 - 需要改進</option>
<option value="2">2分 - 基本達標</option>
<option value="3">3分 - 良好</option>
<option value="4">4分 - 優秀</option>
<option value="5">5分 - 卓越</option>
</select>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-star me-2"></i>提交回饋
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">STAR方法說明</h5>
</div>
<div class="card-body">
<p class="text-muted">STAR方法是一種結構化的回饋方式</p>
<ul class="list-unstyled">
<li><strong>S</strong> - Situation: 描述當時的情況</li>
<li><strong>T</strong> - Task: 說明需要完成的任務</li>
<li><strong>A</strong> - Action: 描述採取的具體行動</li>
<li><strong>R</strong> - Result: 說明最終的結果</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Rankings Section -->
<div id="rankings-section" class="section" style="display: none;">
<div class="row">
<div class="col-12">
<h2 class="mb-4">
<i class="bi bi-trophy-fill me-2"></i>排名系統
</h2>
</div>
</div>
<!-- Advanced Filters -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-funnel me-2"></i>高級篩選
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<label for="rankingDepartment" class="form-label">部門</label>
<select class="form-select" id="rankingDepartment">
<option value="">全部部門</option>
<option value="IT">IT部門</option>
<option value="HR">人力資源部</option>
<option value="Finance">財務部</option>
<option value="Marketing">行銷部</option>
<option value="Sales">業務部</option>
<option value="Operations">營運部</option>
</select>
</div>
<div class="col-md-3">
<label for="rankingPosition" class="form-label">職位</label>
<input type="text" class="form-control" id="rankingPosition" placeholder="輸入職位關鍵字">
</div>
<div class="col-md-2">
<label for="minPoints" class="form-label">最低積分</label>
<input type="number" class="form-control" id="minPoints" placeholder="0">
</div>
<div class="col-md-2">
<label for="maxPoints" class="form-label">最高積分</label>
<input type="number" class="form-control" id="maxPoints" placeholder="無限制">
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-grid">
<button class="btn btn-primary" id="applyFilters">
<i class="bi bi-search me-1"></i>篩選
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Statistics Summary -->
<div class="row mb-4" id="rankingStats" style="display: none;">
<div class="col-md-2">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<h6 class="card-title">總人數</h6>
<h4 id="totalCount">0</h4>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h6 class="card-title">平均積分</h6>
<h4 id="avgPoints">0</h4>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-info text-white">
<div class="card-body text-center">
<h6 class="card-title">中位數</h6>
<h4 id="medianPoints">0</h4>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-warning text-white">
<div class="card-body text-center">
<h6 class="card-title">最高分</h6>
<h4 id="maxPoints">0</h4>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-secondary text-white">
<div class="card-body text-center">
<h6 class="card-title">最低分</h6>
<h4 id="minPoints">0</h4>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card bg-dark text-white">
<div class="card-body text-center">
<h6 class="card-title">標準差</h6>
<h4 id="stdDev">0</h4>
</div>
</div>
</div>
</div>
<!-- Rankings List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-list-ol me-2"></i>排名列表
</h5>
</div>
<div class="card-body">
<div id="rankingsList">
<div class="text-center text-muted py-4">
<i class="bi bi-trophy" style="font-size: 3rem;"></i>
<p class="mt-2">點擊篩選按鈕查看排名</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Admin Section -->
<div id="admin-section" class="section" style="display: none;">
<div class="row">
<div class="col-12">
<h2 class="mb-4">
<i class="bi bi-gear-fill me-2"></i>系統管理
</h2>
</div>
</div>
<!-- Statistics Overview -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">總用戶數</h4>
<h2 id="adminTotalUsers">0</h2>
</div>
<div class="align-self-center">
<i class="bi bi-people-fill" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">活躍用戶</h4>
<h2 id="adminActiveUsers">0</h2>
</div>
<div class="align-self-center">
<i class="bi bi-person-check-fill" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">總評估數</h4>
<h2 id="adminTotalAssessments">0</h2>
</div>
<div class="align-self-center">
<i class="bi bi-clipboard-check" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h4 class="card-title">總回饋數</h4>
<h2 id="adminTotalFeedbacks">0</h2>
</div>
<div class="align-self-center">
<i class="bi bi-star-fill" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Management Tabs -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" id="adminTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="users-tab" data-bs-toggle="tab" data-bs-target="#users" type="button" role="tab">
<i class="bi bi-people me-2"></i>用戶管理
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="capabilities-management-tab" data-bs-toggle="tab" data-bs-target="#capabilities-management" type="button" role="tab">
<i class="bi bi-list-check me-2"></i>能力項目管理
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="dept-capabilities-tab" data-bs-toggle="tab" data-bs-target="#dept-capabilities" type="button" role="tab">
<i class="bi bi-diagram-3 me-2"></i>部門能力設定
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="statistics-tab" data-bs-toggle="tab" data-bs-target="#statistics" type="button" role="tab">
<i class="bi bi-graph-up me-2"></i>統計分析
</button>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content" id="adminTabContent">
<div class="tab-pane fade show active" id="users" role="tabpanel">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">用戶列表</h5>
<button class="btn btn-primary" id="refreshUsersBtn">
<i class="bi bi-arrow-clockwise me-1"></i>刷新
</button>
</div>
<div id="usersManagement">
<div class="text-center text-muted py-4">
<i class="bi bi-people" style="font-size: 3rem;"></i>
<p class="mt-2">點擊刷新按鈕載入用戶列表</p>
</div>
</div>
</div>
<div class="tab-pane fade" id="capabilities-management" role="tabpanel">
<div class="row mb-3">
<div class="col-12">
<h5><i class="bi bi-upload me-2"></i>匯入能力項目 (CSV)</h5>
<div class="alert alert-info">
<strong>CSV 格式說明:</strong>
<ul class="mb-0 mt-2">
<li>第一行為標題:<code>name,l1_description,l2_description,l3_description,l4_description,l5_description</code></li>
<li>每一行代表一個能力項目</li>
<li>範例:<code>溝通能力,基本溝通,有效溝通,專業溝通,領導溝通,戰略溝通</code></li>
</ul>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-8">
<input type="file" class="form-control" id="csvFileInput" accept=".csv">
</div>
<div class="col-md-4">
<button class="btn btn-primary" id="importCsvBtn">
<i class="bi bi-upload me-1"></i>匯入 CSV
</button>
<button class="btn btn-outline-secondary" id="downloadTemplateCsvBtn">
<i class="bi bi-download me-1"></i>下載範本
</button>
</div>
</div>
<hr>
<div class="row">
<div class="col-12">
<h5>現有能力項目</h5>
<button class="btn btn-success btn-sm mb-3" id="refreshCapabilitiesBtn">
<i class="bi bi-arrow-clockwise me-1"></i>刷新列表
</button>
<div id="capabilitiesList">
<div class="text-center text-muted py-4">
<i class="bi bi-list-check" style="font-size: 3rem;"></i>
<p class="mt-2">點擊刷新按鈕載入能力項目列表</p>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="dept-capabilities" role="tabpanel">
<div class="row mb-3">
<div class="col-md-4">
<label for="deptSelect" class="form-label"><strong>選擇部門</strong></label>
<select class="form-select" id="deptSelect">
<option value="">-- 請選擇部門 --</option>
<option value="IT">IT部門</option>
<option value="HR">人力資源部</option>
<option value="Finance">財務部</option>
<option value="Marketing">行銷部</option>
<option value="Sales">業務部</option>
<option value="Operations">營運部</option>
</select>
</div>
<div class="col-md-8 d-flex align-items-end">
<button class="btn btn-success me-2" id="saveDeptCapabilitiesBtn" disabled>
<i class="bi bi-check-circle me-1"></i>儲存設定
</button>
<button class="btn btn-outline-secondary" id="loadDeptCapabilitiesBtn" disabled>
<i class="bi bi-arrow-clockwise me-1"></i>載入現有設定
</button>
</div>
</div>
<hr>
<div id="capabilitiesCheckboxList">
<div class="text-center text-muted py-4">
<i class="bi bi-diagram-3" style="font-size: 3rem;"></i>
<p class="mt-2">請先選擇部門</p>
</div>
</div>
</div>
<div class="tab-pane fade" id="statistics" role="tabpanel">
<div class="row">
<div class="col-md-6">
<h5 class="mb-3">部門分布</h5>
<div id="departmentStats">
<div class="text-center text-muted py-3">
<i class="bi bi-pie-chart" style="font-size: 2rem;"></i>
<p class="mt-2">載入中...</p>
</div>
</div>
</div>
<div class="col-md-6">
<h5 class="mb-3">積分統計</h5>
<div id="pointsStats">
<div class="text-center text-muted py-3">
<i class="bi bi-bar-chart" style="font-size: 2rem;"></i>
<p class="mt-2">載入中...</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Spinner -->
<div id="loadingSpinner" class="position-fixed top-50 start-50 translate-middle" style="display: none; z-index: 9999;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">載入中...</span>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<i class="bi bi-info-circle-fill text-primary me-2"></i>
<strong class="me-auto">系統通知</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body" id="toastMessage">
<!-- Toast message will be inserted here -->
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
</body>
</html>