# Design: add-resource-workload ## Architecture Overview ``` ┌─────────────┐ ┌─────────────────────┐ ┌───────────────┐ │ Frontend │────▶│ Workload API │────▶│ MySQL │ │ (Heatmap) │ │ /api/v1/workload │ │ (Snapshots) │ └─────────────┘ └─────────────────────┘ └───────────────┘ │ ▼ ┌───────────────┐ │ Redis │ │ (Cache) │ └───────────────┘ ``` ## Key Design Decisions ### 1. 負載計算策略:即時計算 vs 快照 **決定**:採用**混合策略** - **即時計算**:API 請求時計算,結果快取 1 小時 - **快照儲存**:每日凌晨儲存歷史快照供趨勢分析 **理由**: - 即時計算確保數據新鮮度 - 快照提供歷史趨勢分析能力 - Redis 快取減少計算負擔 ### 2. 週邊界定義 **決定**:採用 **ISO 8601 週**(週一至週日) **理由**: - 國際標準,避免歧義 - Python/MySQL 原生支援 - 便於未來國際化 ### 3. 負載計算公式 ``` 週負載 = Σ(該週到期任務的 original_estimate) / 週容量 × 100% ``` **任務計入規則**: - `due_date` 在該週範圍內 - `assignee_id` 為目標使用者 - `status` 非已完成狀態 **邊界情況處理**: - `original_estimate` 為空:計為 0(不計入負載) - `capacity` 為 0:顯示為 N/A(避免除以零) ### 4. 負載等級閾值 | 等級 | 範圍 | 顏色 | 描述 | |------|------|------|------| | normal | 0-79% | green | 正常 | | warning | 80-99% | yellow | 警告 | | overloaded | ≥100% | red | 超載 | ### 5. 快取策略 ``` 快取鍵格式:workload:{user_id}:{week_start} TTL:3600 秒(1 小時) ``` **失效時機**: - 任務建立/更新/刪除 - 使用者容量變更 **實作**:暫不實作主動失效,依賴 TTL 自然過期(Phase 1 簡化) ### 6. 權限控制 | 角色 | 可查看範圍 | |------|-----------| | super_admin | 所有使用者 | | manager | 同部門使用者 | | engineer | 僅自己 | ## API Design ### GET /api/v1/workload/heatmap 查詢團隊負載熱圖 **Query Parameters**: - `week_start`: ISO 日期(預設當週一) - `department_id`: 部門篩選(可選) - `user_ids`: 使用者 ID 陣列(可選) **Response**: ```json { "week_start": "2024-01-01", "week_end": "2024-01-07", "users": [ { "user_id": "uuid", "user_name": "John Doe", "department_id": "uuid", "department_name": "R&D", "capacity_hours": 40.0, "allocated_hours": 32.5, "load_percentage": 81.25, "load_level": "warning", "task_count": 5 } ] } ``` ### GET /api/v1/workload/user/{user_id} 查詢特定使用者的負載詳情 **Query Parameters**: - `week_start`: ISO 日期 **Response**: ```json { "user_id": "uuid", "week_start": "2024-01-01", "capacity_hours": 40.0, "allocated_hours": 32.5, "load_percentage": 81.25, "load_level": "warning", "tasks": [ { "task_id": "uuid", "title": "Task Name", "project_name": "Project A", "due_date": "2024-01-05", "original_estimate": 8.0, "status": "in_progress" } ] } ``` ### PUT /api/v1/users/{user_id}/capacity 更新使用者容量 **Request Body**: ```json { "capacity": 32.0, "effective_from": "2024-01-08", "effective_until": "2024-01-14", "reason": "年假" } ``` ## Data Model ### pjctrl_workload_snapshots 儲存歷史負載快照 ```sql CREATE TABLE pjctrl_workload_snapshots ( id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) NOT NULL, week_start DATE NOT NULL, allocated_hours DECIMAL(8,2) NOT NULL DEFAULT 0, capacity_hours DECIMAL(8,2) NOT NULL DEFAULT 40, load_percentage DECIMAL(5,2) NOT NULL DEFAULT 0, task_count INT NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES pjctrl_users(id) ON DELETE CASCADE, UNIQUE KEY uk_user_week (user_id, week_start), INDEX idx_week_start (week_start) ); ``` ### pjctrl_capacity_adjustments(可選,Phase 1 暫不實作) 儲存臨時容量調整(如請假) ```sql CREATE TABLE pjctrl_capacity_adjustments ( id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) NOT NULL, week_start DATE NOT NULL, adjusted_capacity DECIMAL(5,2) NOT NULL, reason VARCHAR(200), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES pjctrl_users(id) ON DELETE CASCADE, UNIQUE KEY uk_user_week (user_id, week_start) ); ``` ## Implementation Notes ### 後端結構 ``` backend/app/ ├── api/ │ └── workload/ │ ├── __init__.py │ └── router.py ├── services/ │ └── workload_service.py ├── models/ │ └── workload_snapshot.py └── schemas/ └── workload.py ``` ### 週起始日計算 ```python from datetime import date, timedelta def get_week_start(d: date) -> date: """取得 ISO 週的週一""" return d - timedelta(days=d.weekday()) ``` ### Redis 快取範例 ```python async def get_user_workload(user_id: str, week_start: date) -> dict: cache_key = f"workload:{user_id}:{week_start}" cached = redis_client.get(cache_key) if cached: return json.loads(cached) result = calculate_workload(user_id, week_start) redis_client.setex(cache_key, 3600, json.dumps(result)) return result ```